initial monorepo commit (fe + be)

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

View File

@@ -0,0 +1,169 @@
# Integrasi Tabel billing_dpjp
## Ringkasan Perubahan
Integrasi tabel `billing_dpjp` yang baru ke dalam sistem billing CareIT. Tabel ini digunakan untuk menyimpan Doctor In Charge (DPJP) untuk setiap billing.
## Perubahan yang Dilakukan
### 1. **services/billing_pasien.go**
#### Function: `DataFromFE()`
- **Tambahan**: Kode untuk insert ID_DPJP ke tabel `billing_dpjp` (lines ~439-451)
- **Logika**: Jika `input.ID_DPJP > 0`, maka insert record ke tabel `billing_dpjp`
- **Log**: Menambah logging untuk tracking DPJP insertion
```go
if input.ID_DPJP > 0 {
billingDPJP := models.Billing_DPJP{
ID_Billing: billing.ID_Billing,
ID_DPJP: input.ID_DPJP,
}
// ... insert logic
}
```
#### Function: `GetBillingDetailAktifByNama()`
- **Perubahan**: Menambah return value (dari 8 return menjadi 9)
- **Sebelumnya**: `(*models.BillingPasien, []string, []string, []string, []string, []string, []string, error)`
- **Sesudahnya**: `(*models.BillingPasien, []string, []string, []string, []string, []string, []string, int, error)`
- **Tambahan Query**:
- Query dari tabel `billing_dpjp` untuk fetch ID_DPJP
- Jika tidak ada DPJP, return value 0 (normal)
```go
var dpjpRow struct {
ID_DPJP int `gorm:"column:ID_DPJP"`
}
// Query dari billing_dpjp
```
### 2. **services/riwayat_billing_pasien.go**
#### Function: `GetRiwayatPasienAll()`
- **Tambahan Query**: Menambah query untuk fetch DPJP dari tabel `billing_dpjp`
- **Map Creation**: Membuat `dpjpMap` untuk mapping ID_Billing ke ID_DPJP
- **Response Update**: Field `ID_DPJP` di response sudah diisi (meskipun belum direferensikan di item construction, field sudah ada di model)
```go
dpjpMap := make(map[int]int)
// Query dari billing_dpjp untuk mendapatkan ID_DPJP
```
### 3. **handlers/routes.go**
#### Function: `GetBillingAktifByNamaHandler()`
- **Update Parameter**: Menangkap return value baru (ID_DPJP)
- **Response Update**: Menambah field `id_dpjp` dalam JSON response
```go
billing, tindakan, icd9, icd10, dokter, inacbgRI, inacbgRJ, dpjp, err := services.GetBillingDetailAktifByNama(nama)
```
Response JSON sekarang include:
```json
{
"data": {
"billing": {...},
"tindakan_rs": [...],
"icd9": [...],
"icd10": [...],
"dokter": [...],
"inacbg_ri": [...],
"inacbg_rj": [...],
"id_dpjp": 123
}
}
```
## Model yang Sudah Ada
### `models/models.go`
- **Billing_DPJP**: Struct untuk tabel `billing_dpjp` (line ~61-68)
```go
type Billing_DPJP struct {
ID_Billing int `gorm:"column:ID_Billing;primaryKey"`
ID_DPJP int `gorm:"column:ID_DPJP;primaryKey"`
}
```
- **BillingRequest**: Sudah memiliki field `ID_DPJP` (line ~225)
```go
ID_DPJP int `json:"id_dpjp"`
```
- **Riwayat_Pasien_all**: Sudah memiliki field `ID_DPJP` (line ~183)
```go
ID_DPJP string `json:"id_dpjp"`
```
## API Endpoint yang Terpengaruh
### GET /billing/aktif
**Parameter**: `nama_pasien`
**Response**:
```json
{
"status": "success",
"data": {
"billing": {...},
"id_dpjp": 123,
...
}
}
```
## Database Schema
Tabel `billing_dpjp` sudah dibuat di PostgreSQL dengan struktur:
```sql
CREATE TABLE billing_dpjp (
ID_Billing integer NOT NULL,
ID_DPJP integer NOT NULL,
PRIMARY KEY (ID_Billing, ID_DPJP),
FOREIGN KEY (ID_Billing) REFERENCES billing_pasien(ID_Billing),
FOREIGN KEY (ID_DPJP) REFERENCES dokter(ID_Dokter)
);
```
## Testing
### Test Case 1: Membuat Billing Baru dengan DPJP
**POST** `/billing`
```json
{
"id_dpjp": 5,
"nama_dokter": ["Dr. Budi"],
...
}
```
**Expected**: DPJP tercatat di tabel `billing_dpjp`
### Test Case 2: Fetch Billing Aktif dengan DPJP
**GET** `/billing/aktif?nama_pasien=John Doe`
**Expected**: Response include field `id_dpjp` dengan value yang benar
### Test Case 3: Riwayat Billing Tertutup
**GET** `/admin/riwayat-pasien-all`
**Expected**: Setiap billing dalam response include DPJP (jika ada)
## File yang Dimodifikasi
1. ✅ `services/billing_pasien.go` - DataFromFE() + GetBillingDetailAktifByNama()
2. ✅ `services/riwayat_billing_pasien.go` - GetRiwayatPasienAll()
3. ✅ `handlers/routes.go` - GetBillingAktifByNamaHandler()
## Status Kompilasi
✅ **BUILD SUCCESS** - Tidak ada error atau warning saat compile `go build .`
## Catatan
- Field `ID_DPJP` di model `Riwayat_Pasien_all` bertipe `string` (line 183) sedangkan di `billing_dpjp` bertipe `int`. Ini mungkin perlu di-harmonisasi untuk konsistensi tipe data di masa depan.
- Fungsi update billing yang ada (`EditPasienComplete`) belum include logika update DPJP. Jika ada requirement untuk update DPJP setelah billing dibuat, perlu ditambahkan.
- DPJP bersifat opsional (jika tidak ada, return 0 - tidak error).

View File

@@ -0,0 +1,154 @@
## 🔍 HASIL CECK LENGKAP - Filter Tanggal Issue
### 📋 RINGKASAN MASALAH
Frontend filter untuk tanggal tidak bekerja karena:
- **Frontend** mengharapkan field: `tanggal_masuk` atau `tanggal_keluar`
- **Backend API** `/admin/riwayat-billing` mengembalikan response dari struct `Request_Admin_Inacbg`
- **Struct `Request_Admin_Inacbg`** TIDAK memiliki field tanggal sama sekali!
---
## 🔗 FLOW API YANG SEBENARNYA
```
Frontend getRiwayatBilling()
API: GET /admin/riwayat-billing
Handler: GetRiwayatBillingHandler
Service: GetAllRiwayatpasien(db)
Return: []models.Request_Admin_Inacbg
Response JSON berisi: id_billing, nama_pasien, id_pasien, kelas, ruangan, total_tarif_rs,
total_klaim, id_dpjp, tindakan_rs, icd9, icd10, inacbg_ri, inacbg_rj,
billing_sign, nama_dokter
❌ TIDAK ADA: tanggal_masuk, tanggal_keluar
```
---
## 📦 PERBANDINGAN 2 API ENDPOINT
### Endpoint 1: `/admin/riwayat-billing` (YANG DIPAKAI FRONTEND)
- Handler: `GetRiwayatBillingHandler`
- Service: `GetAllRiwayatpasien()`
- Return Type: `[]models.Request_Admin_Inacbg`
- Fields: ❌ Tanggal fields TIDAK ADA
### Endpoint 2: `/admin/riwayat-pasien-all` (TIDAK DIPAKAI)
- Handler: `GetRiwayatPasienAllHandler`
- Service: `GetRiwayatPasienAll()`
- Return Type: `[]models.Riwayat_Pasien_all`
- Fields: ✅ Memiliki Tanggal_Masuk (*time.Time) dan Tanggal_Keluar (string)
- **ISSUE**: Di service GetRiwayatPasienAll(), baris 210 ada bug:
```go
Tanggal_Masuk: b.Tanggal_masuk, // ❌ Assign pointer langsung, harusnya .Format("2006-01-02")
```
---
## 🗂️ FILE STRUCTURE
### Backend Services
**File: riwayat_billing_pasien.go**
- Line 10: `func GetRiwayatPasienAll()` → returns `[]Riwayat_Pasien_all` ✅ Ada tanggal
- Line 226: `func GetAllRiwayatpasien()` → returns `[]Request_Admin_Inacbg` ❌ Tanpa tanggal
### Models
**File: models.go**
- Line 175: `type Riwayat_Pasien_all struct`
- Memiliki: Tanggal_Masuk (*time.Time), Tanggal_Keluar (string)
- STATUS: Fields exists tapi tidak di-populate di service
- Line 314: `type Request_Admin_Inacbg struct`
- TIDAK memiliki field tanggal apapun
- STATUS: Ini yang dipakai GetAllRiwayatpasien()
### Handlers
**File: handlers/routes.go**
- Line 56: `GET /admin/riwayat-billing` → calls `GetAllRiwayatpasien()`
- Line 58: `GET /admin/riwayat-pasien-all` → calls `GetRiwayatPasienAll()`
### Frontend
**File: riwayat-billing-pasien.tsx**
- Line 5: imports `getRiwayatBilling()`
- Line 87: calls `getRiwayatBilling()`
**File: lib/api-helper.ts**
- Line 252: `getRiwayatBilling()` → calls `/admin/riwayat-billing`
---
## 🎯 ROOT CAUSE
Frontend dipaksa menggunakan API `/admin/riwayat-billing` yang:
1. Memanggil `GetAllRiwayatpasien()`
2. Mengembalikan struct `Request_Admin_Inacbg` yang tidak punya field tanggal
3. Menyebabkan frontend tidak bisa filter berdasarkan tanggal
**Ada 2 API endpoint dengan data berbeda:**
- `/admin/riwayat-billing` → untuk INACBG Admin (tidak ada tanggal)
- `/admin/riwayat-pasien-all` → untuk riwayat lengkap (ada tanggal tapi ada bug di service)
---
## ✅ DATABASE - FIELDS TERSEDIA
**Table: billing_pasien**
```sql
Tanggal_Masuk (TIMESTAMP) ✅ Ada di database
Tanggal_Keluar (TIMESTAMP) ✅ Ada di database
```
**Struct: BillingPasien**
```go
Tanggal_masuk *time.Time ✅ Mapped dari Tanggal_Masuk
Tanggal_keluar *time.Time ✅ Mapped dari Tanggal_Keluar
```
---
## 📊 CHECKLIST STATUS
| Item | Status | Lokasi | Catatan |
|------|--------|--------|---------|
| Database fields (tanggal) | ✅ Ada | billing_pasien table | Tersedia di DB |
| BillingPasien struct fields | ✅ Ada | models.go | Mapped dengan benar |
| Riwayat_Pasien_all struct | ✅ Ada | models.go L175 | Punya field tanggal |
| Request_Admin_Inacbg struct | ❌ Tidak | models.go L314 | Tidak punya field tanggal |
| GetRiwayatPasienAll service | ✅ Ada tapi BUG | services L10 | Line 210: bug assign pointer |
| GetAllRiwayatpasien service | ✅ Ada | services L226 | Tapi return struct tanpa tanggal |
| `/admin/riwayat-billing` endpoint | ✅ Ada | handlers L56 | Pakai GetAllRiwayatpasien |
| `/admin/riwayat-pasien-all` endpoint | ✅ Ada | handlers L58 | Tidak dipakai frontend |
| Frontend getRiwayatBilling() | ✅ Ada | api-helper.ts L252 | Call `/admin/riwayat-billing` |
| Frontend filter logic | ✅ Ada | riwayat-billing-pasien.tsx | Ready to work jika data ada |
| Frontend filter UI | ✅ Ada | riwayat-billing-pasien.tsx | Consolidated dropdown ✅ |
---
## 🤔 KESIMPULAN
**Masalah Core:**
Frontend menggunakan endpoint `/admin/riwayat-billing` yang return struct tanpa field tanggal.
Padahal ada endpoint alternatif `/admin/riwayat-pasien-all` yang punya field tanggal.
**Opsi Solusi:**
1. **Update `Request_Admin_Inacbg` struct** - tambahkan field tanggal
- Perubahan minimal: models.go (add 2 fields)
- Update: GetAllRiwayatpasien() service untuk assign tanggal values
2. **Switch frontend ke `/admin/riwayat-pasien-all`**
- Update: api-helper.ts getRiwayatBilling()
- Fix bug di GetRiwayatPasienAll() service (line 210)
3. **Tunggu user decision** ⏳ (current state)

View File

@@ -0,0 +1,146 @@
# Debugging Guide: Warning Billing Sign Not Updating
## Langkah-Langkah Debug:
### 1. Buka Console Browser (F12)
- Tekan `F12` → Tab **Console**
- Pastikan console kosong (tidak ada error merah)
### 2. Test Flow:
1. Buka halaman **Billing Pasien**
2. **Cari pasien** yang sudah punya billing history
3. **Buka Edit Modal** (klik tombol edit)
4. **Ubah tindakan** (tambah atau hapus)
5. **Klik Simpan Perubahan**
6. **Tunggu** dan lihat console logs
### 3. Baca Console Logs Dalam Order:
#### A. Pada saat klik Simpan:
Cari logs dengan pattern:
```
🚀 Mengirim request ke backend:
```
**Verify:**
- URL benar: `/billing/{id}`
- Method: PUT
- `total_tarif_rs_value` ada dan tidak undefined
- Value-nya angka (bukan 0 jika ada tindakan)
#### B. Setelah Simpan Berhasil:
Cari logs dengan pattern:
```
📢 Billing data updated event received:
```
**Ini menandakan:** Event berhasil di-dispatch dari modal
Kemudian cari:
```
🔄 Triggering loadBillingAktifHistory for:
```
**Ini menandakan:** Event listener di-trigger
#### C. API Response Check:
Cari logs dengan pattern:
```
📡 Full API Response:
```
**Ini menunjukkan:** Response dari GET /billing/aktif
**Buka detail:** Expand object dan cari:
- `data.billing.total_tarif_rs` - harus nilai baru (bukan 0)
- `data.billing.total_klaim` - total BPJS
#### D. Tarif Extraction:
Cari logs dengan pattern:
```
🔍 Tarif Extraction Debug:
```
**Verify:**
- `storedTotalTarif` - apakah value dari API terambil?
- `calculatedTotalTarif` - berapa nilai calculated-nya?
- `finalTotalTarif` - mana yang dipakai (stored atau calculated)?
#### E. State Update:
Cari logs dengan pattern:
```
💾 Setting TotalTarifRS:
💾 Setting BillingHistory with:
```
**Verify:**
- Value yang di-set berapa?
- Apakah nilai yang di-set berbeda dari nilai sebelumnya?
#### F. Live Values:
Cari logs dengan pattern:
```
📈 Live Values Changed:
```
**Verify:**
- `totalTarifRS` - apakah berubah dari sebelumnya?
- `totalKlaimBPJSLive` - berapa nilainya?
- `liveBillingSign` - berapa hasilnya (Hijau/Kuning/Merah)?
### 4. Jika Ada Error:
Cari logs merah (error) atau orange (warning). Catat isi-nya persis.
### 5. Screenshot/Copy Console Output:
Setelah test selesai:
1. Right-click di console → "Save as..."
2. Atau screenshot dari Browser DevTools
3. Share dengan informasi:
- Nama pasien yang di-test
- Tindakan apa yang di-ubah
- Console log output lengkap
## Possible Issues & Solutions:
### Issue 1: Event tidak ter-trigger
**Log yang terlihat:** Tidak ada `📢 Billing data updated event received:`
**Penyebab:**
- Modal tidak dispatch event
- Event listener tidak register
**Fix:** Check di modal if `window.dispatchEvent` di-execute
### Issue 2: API Response tidak punya total_tarif_rs baru
**Log yang terlihat:** `📡 Full API Response` menunjukkan `total_tarif_rs` = 0 atau nilai lama
**Penyebab:**
- Backend tidak menerima `total_tarif_rs` di request
- Backend tidak update ke database
**Fix:** Check apakah request PUT ke backend include `total_tarif_rs`
### Issue 3: State tidak update walaupun API response benar
**Log yang terlihat:** `finalTotalTarif` benar tapi `💾 Setting TotalTarifRS` tidak ada
**Penyebab:**
- Exception di loadBillingAktifHistory
- setState tidak execute
**Fix:** Check console untuk error messages
### Issue 4: State update tapi UI tidak berubah
**Log yang terlihat:** `📈 Live Values Changed` menunjukkan nilai baru tapi card masih lama
**Penyebab:**
- Component tidak re-render
- UI logic salah
**Fix:** Check React DevTools untuk component state
---
## Copy-Paste Template untuk Report:
```
## Test Result:
- Pasien: [nama pasien yang di-test]
- Tindakan yang di-ubah: [apa yang di-ubah]
### Console Logs:
[Paste semua console log output di sini]
### Observations:
1. Event triggered? [Ya/Tidak]
2. API response punya total_tarif_rs baru? [Ya/Tidak, value: ...]
3. State terupdate? [Ya/Tidak, value: ...]
4. UI berubah? [Ya/Tidak]
### Error Messages (jika ada):
[Paste error message di sini]
```

View File

@@ -0,0 +1,125 @@
# 🔍 ANALISIS ERROR: Column "tanggal_keluar" does not exist
## 📌 ERROR YANG TERJADI
```
ERROR: column "tanggal_keluar" does not exist (SQLSTATE 42703)
```
Muncul di halaman: **Riwayat Billing Pasien**
---
## 🎯 AKAR PENYEBAB MASALAH (ROOT CAUSE)
### ✅ Yang sudah diperiksa:
1. **File SQL PostgreSQL** - Tabel `billing_pasien` sudah benar:
- Column ada dengan nama: `"Tanggal_Keluar"` (dengan capital K)
- Struktur table di [postgreSQL_CareIt.sql](postgreSQL_CareIt.sql#L129-L140)
2. **Models Golang** - BillingPasien struct benar:
- Field: `Tanggal_keluar *time.Time` dengan mapping column: `Tanggal_Keluar`
- Lokasi: [models.go](backendcareit_v4/models/models.go#L189)
3. **Service File** - Query yang menggunakan column ini:
- File: [riwayat_billing_pasien.go](backendcareit_v4/services/riwayat_billing_pasien.go#L13-L14)
- File: [billing_aktif.go](backendcareit_v4/services/billing_aktif.go#L12-L13)
### ❌ MASALAH YANG DITEMUKAN:
**Ada perbedaan case sensitivity antara query dan column name di PostgreSQL!**
```
Query di Golang: "Tanggal_Keluar IS NOT NULL"
Column di Database: "Tanggal_Keluar" (dengan double quotes)
```
Ini adalah **karakteristik PostgreSQL yang CASE SENSITIVE**:
```
✗ SALAH: Tanggal_Keluar (tanpa quotes → dianggap lowercase)
✓ BENAR: "Tanggal_Keluar" (dengan quotes → exactly matching)
```
---
## 📍 TEMPAT ERROR TERJADI
**File:** [backendcareit_v4/services/riwayat_billing_pasien.go](backendcareit_v4/services/riwayat_billing_pasien.go)
**Line 13-14:**
```go
// ❌ SALAH - Query tanpa quotes:
if err := db.Where("Tanggal_Keluar IS NOT NULL").Find(&billings).Error; err != nil {
return nil, err
}
```
**Line 179:**
```go
// ❌ SALAH - Query tanpa quotes:
if err := db.Where("Tanggal_Keluar IS NOT NULL").Find(&billings).Error; err != nil {
return nil, err
}
```
**File:** [backendcareit_v4/services/billing_aktif.go](backendcareit_v4/services/billing_aktif.go)
**Line 13:**
```go
// ❌ SALAH - Query tanpa quotes:
if err := db.Where("Tanggal_Keluar IS NULL").Find(&billings).Error; err != nil {
return nil, err
}
```
---
## 🔧 SOLUSI
### Opsi A: Update Query dengan Double Quotes (RECOMMENDED)
Ganti semua query tanpa quotes menjadi:
```go
db.Where(`"Tanggal_Keluar" IS NOT NULL`)
db.Where(`"Tanggal_Keluar" IS NULL`)
```
### Opsi B: Update Column Names di PostgreSQL
Ubah PostgreSQL column dari quoted names menjadi lowercase:
```sql
ALTER TABLE billing_pasien RENAME COLUMN "Tanggal_Keluar" TO tanggal_keluar;
```
---
## 📋 SUMMARY
| Aspek | Status |
|-------|--------|
| Database PostgreSQL | ✅ Sudah ada column dengan benar |
| File SQL Import | ✅ Struktur sudah benar |
| Models Golang | ✅ Mapping sudah benar |
| Query di Services | ❌ MISSING QUOTES untuk PostgreSQL |
**Penyebab utama:** PostgreSQL require double quotes untuk identifier yang case-sensitive, sementara query saat ini tidak menggunakannya.
---
## 📌 NEXT STEP
Setelah Anda confirm, saya akan melakukan FIX dengan:
1. Update semua query di `billing_aktif.go` (1 tempat)
2. Update semua query di `riwayat_billing_pasien.go` (2 tempat)
3. Test ulang aplikasi
**File yang akan diubah:**
- `backendcareit_v4/services/billing_aktif.go`
- `backendcareit_v4/services/riwayat_billing_pasien.go`
---
**Status:** ⏳ Menunggu konfirmasi dari Anda sebelum melakukan perubahan

View File

@@ -0,0 +1,183 @@
# Fix Riwayat Billing Pasien - Menampilkan Field yang Hilang
## Problem
Di halaman Riwayat Billing Pasien, beberapa field tidak menampilkan data:
- ❌ Kelas
- ❌ DPJP (Doctor In Charge)
- ❌ Total Tarif RS
- ❌ Total Klaim
## Root Cause
Backend sudah query dan return data (field ada di model), tapi frontend tidak menampilkannya.
## Solusi
### Backend Changes
#### 1. **models/models.go**
- **Tambah field** `ID_DPJP` ke struct `Request_Admin_Inacbg` (line ~315)
```go
type Request_Admin_Inacbg struct {
// ... existing fields
ID_DPJP int `json:"id_dpjp"`
// ...
}
```
#### 2. **services/riwayat_billing_pasien.go** - Function `GetAllRiwayatpasien()`
- **Tambah query** untuk fetch DPJP dari tabel `billing_dpjp` (after dokter query)
```go
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
}
```
- **Update compilation** untuk include `ID_DPJP` di response (line ~365)
```go
item := models.Request_Admin_Inacbg{
// ... existing fields
ID_DPJP: dpjpMap[b.ID_Billing],
// ...
}
```
### Frontend Changes
#### **src/app/component/riwayat-billing-pasien.tsx**
1. **Update BillingData Interface** (line ~14-35)
- Tambah field `kelas` (lowercase variant)
- Tambah field `ID_DPJP` dan `id_dpjp`
2. **Mobile Card View** (after Dokter section)
- Tambah display untuk Kelas
- Tambah display untuk DPJP
- Tambah display untuk Total Tarif RS
- Tambah display untuk Total Klaim
```tsx
{/* Kelas */}
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide">
Kelas
</span>
<span className="text-sm font-semibold text-[#2591D0]">
{item.Kelas || item.kelas || '-'}
</span>
</div>
{/* DPJP */}
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide">
DPJP
</span>
<span className="text-sm font-semibold text-[#2591D0]">
{item.ID_DPJP || item.id_dpjp ? `ID: ${item.ID_DPJP || item.id_dpjp}` : '-'}
</span>
</div>
{/* Total Tarif RS */}
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide">
Total Tarif RS
</span>
<span className="text-sm font-semibold text-[#2591D0]">
{item.total_tarif_rs ? `Rp ${item.total_tarif_rs?.toLocaleString('id-ID')}` : '-'}
</span>
</div>
{/* Total Klaim */}
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide">
Total Klaim
</span>
<span className="text-sm font-semibold text-[#2591D0]">
{item.total_klaim ? `Rp ${item.total_klaim?.toLocaleString('id-ID')}` : '-'}
</span>
</div>
```
## Data Flow
```
GET /admin/riwayat-billing
GetRiwayatBillingHandler
GetAllRiwayatpasien()
├─ Query billing_pasien
├─ Query pasien data (nama, kelas, ruangan)
├─ Query billing_tindakan, icd9, icd10
├─ Query billing_dokter (nama dokter)
├─ Query billing_dpjp (NEW! - untuk dapatkan ID_DPJP)
└─ Compile response dengan semua field
Response JSON include:
- total_tarif_rs ✅
- total_klaim ✅
- id_dpjp ✅
- kelas ✅
Frontend parse dan display semua field ✅
```
## API Response Example
```json
{
"status": "success",
"data": [
{
"id_billing": 1,
"id_pasien": 5,
"nama_pasien": "John Doe",
"Kelas": "1",
"ruangan": "ICU A",
"total_tarif_rs": 5000000,
"total_klaim": 8000000,
"id_dpjp": 3,
"tindakan_rs": ["Operasi", "Konsultasi"],
"icd9": [...],
"icd10": [...],
"inacbg_ri": [],
"inacbg_rj": ["A10.01"],
"billing_sign": "Hijau",
"nama_dokter": ["Dr. Budi", "Dr. Ani"]
}
]
}
```
## Testing Checklist
- [ ] Backend compile sukses
- [ ] GET `/admin/riwayat-billing` return semua field
- [ ] Field `total_tarif_rs` tampil dengan format currency
- [ ] Field `total_klaim` tampil dengan format currency
- [ ] Field `Kelas` tampil dengan benar
- [ ] Field `id_dpjp` tampil sebagai "ID: X" atau "-" jika tidak ada
- [ ] Mobile card view menampilkan semua field baru
- [ ] Desktop table view tetap normal (mungkin perlu ekspand untuk tampil field baru)
## Files Modified
1. ✅ `backendcareit_v4/models/models.go` - Tambah `ID_DPJP` field
2. ✅ `backendcareit_v4/services/riwayat_billing_pasien.go` - Query DPJP
3. ✅ `frontendcareit_v4/src/app/component/riwayat-billing-pasien.tsx` - Display fields
## Status
**Backend Build**: SUCCESS
**All fields now available**: YES
**Frontend Display**: UPDATED

View File

@@ -0,0 +1,134 @@
# Debug Guide: INACBG Modal - Existing Codes Not Displaying
## Problem
When clicking "Edit INACBG" button, the modal opens but existing INACBG codes from the database are not appearing. The modal shows "Belum ada kode INA CBG yang dipilih" instead of displaying saved codes.
## Solution: Comprehensive Logging Added
I've added detailed console logging throughout the data flow to help identify where the data is being lost.
## How to Debug
### Step 1: Open Browser Developer Console
- Press **F12** or Right-click → "Inspect" → "Console" tab
- Keep this open while testing
### Step 2: Trigger the Data Load
1. In the application, select a patient record
2. Load a billing record
3. Look for these console logs in order:
```
📊 Billing data from API: { ... complete object ... }
📊 data.kode_inacbg: (value or undefined)
📊 data.kode_inacbg type: (string, array, undefined, etc)
📊 data.kode_inacbg is Array?: (true/false)
```
**If you see codes here:** ✅ API is returning data correctly
### Step 3: Click "Edit INACBG" Button
You should see:
```
🔓 Edit INACBG button clicked
📋 originalInacbgCodes: [code1, code2, ...]
📋 originalInacbgCodes length: (number)
📋 totalKlaimOriginal: (number)
```
**If you see codes in originalInacbgCodes:** ✅ Parent component has loaded data correctly
### Step 4: Modal Opens
Look for:
```
📋 Edit INACBG Modal opened
📋 currentData received: {
kode_inacbg: [...],
tipe_inacbg: "...",
kelas: "...",
total_klaim: ...
}
📋 currentData.kode_inacbg: [code1, code2, ...]
📋 currentData.kode_inacbg is Array?: true
📋 Codes to set in modal: [code1, code2, ...]
📋 Codes length: (number)
```
### Step 5: Monitor State Changes
Watch for:
```
🎯 selectedInacbgCodes updated: [code1, code2, ...]
🎯 selectedInacbgCodes length: (number)
```
**These logs appear every time the state changes.**
## Diagnostic Checklist
| Log Message | Indicates | If Missing = Problem |
|---|---|---|
| `📊 Billing data from API:` | API returned data successfully | API not returning response |
| `📊 data.kode_inacbg type:` | Format of kode_inacbg from database | Data structure issue |
| `📝 Loaded INACBG codes from DB:` | Codes successfully parsed | Parsing/parsing logic failed |
| `🔓 Edit INACBG button clicked` | Button click registered | Button handler not triggering |
| `originalInacbgCodes length:` (> 0) | Parent component has data | State not loading correctly |
| `📋 currentData received:` | Modal received props | Props not being passed |
| `📋 Codes to set in modal:` | Modal initialized with codes | Modal initialization failed |
| `🎯 selectedInacbgCodes updated:` | Modal state updated with codes | React state not updating |
## Common Issues & Solutions
### Issue 1: API logs show `kode_inacbg: undefined`
**Cause:** Backend not returning `kode_inacbg` field
**Solution:** Check backend API response format
### Issue 2: Codes loaded but `originalInacbgCodes` is empty
**Cause:** State not setting correctly
**Solution:** May need to check React component lifecycle
### Issue 3: Modal shows empty but `currentData received` has codes
**Cause:** Data not flowing from props to component state
**Solution:** Check `setSelectedInacbgCodes` initialization
### Issue 4: `selectedInacbgCodes` shows codes but UI is empty
**Cause:** Rendering logic issue
**Solution:** Check the display condition that shows codes in template
## Data Flow Path
```
Backend Database
API Response (kode_inacbg: [])
INACBG_Admin_Ruangan component loads data
setOriginalInacbgCodes([...])
User clicks "Edit INACBG"
Modal receives: currentData={{ kode_inacbg: originalInacbgCodes, ... }}
edit-INACBG Modal setSelectedInacbgCodes(codes)
UI displays codes with delete buttons
```
## Next Steps
1. **Run the application**
2. **Open F12 console**
3. **Follow the steps above**
4. **Screenshot the console output**
5. **Identify which logs are missing or show wrong values**
6. **Share the logs with specific details about what's missing**
Based on the logs, we can pinpoint exactly where the data flow is broken and fix it accordingly.
## Files Modified
- `INACBG_Admin_Ruangan.tsx` - Added detailed logging at API fetch and button click
- `edit-INACBG.tsx` - Added detailed logging in modal initialization and state updates

View File

@@ -0,0 +1,247 @@
# INACBG Modal Implementation Status
## ✅ Completed Features
### 1. Modal UI Component (edit-INACBG.tsx)
- Full responsive modal design
- INACBG code list display with trash icons
- Delete confirmation modal
- Real-time total klaim calculation
- Responsive for all screen sizes (mobile, tablet, desktop)
### 2. Real-Time Calculation Engine
- **Delta-based approach:** Only new codes added/deleted affect calculation
- **Base calculation:** totalKlaim = original total + sum of new codes
- **Deleted codes:** Subtracted from the delta
- **Visual feedback:** Displays total before and after changes
### 3. Delete Functionality
- Click trash icon to confirm delete
- Confirmation modal before deleting
- Automatic recalculation after delete
- Codes removed from display immediately
### 4. API Integration
- Added endpoints: `getTarifBPJSInacbgRI` and `getTarifBPJSInacbgRJ`
- Fetches tarif data for both RI (Rawat Inap) and RJ (Rawat Jalan)
- Full code descriptions displayed for user reference
### 5. Parent Component Integration
- "Edit INACBG" button in INACBG_Admin_Ruangan component
- Replaced old "Edit" button functionality
- Modal opens/closes correctly
- Integration with existing billing data
### 6. Submit to Backend
- Delta payload calculation:
- New codes added: included in submission
- Existing codes kept: NOT included (database keeps them)
- Deleted codes: marked for deletion with delta
- Backend automatically merges changes with existing data
---
## 🔴 Current Issue: Data Display
### Problem
Existing INACBG codes saved in database are NOT appearing in modal when opened.
### Symptoms
- Modal opens correctly
- Total klaim shows correctly (totalKlaimOriginal is loaded)
- INACBG codes list shows: "Belum ada kode INA CBG yang dipilih"
- User cannot see or delete existing codes
### Root Cause
**UNKNOWN** - Debugging in progress. Data flow broken somewhere between:
1. API returns data →
2. Component loads codes →
3. Modal receives codes →
4. UI displays codes
---
## 🔧 Debugging Infrastructure Added
### Console Logging Points
**In Parent Component (INACBG_Admin_Ruangan.tsx):**
```
1. Line 446-449: API response format check
- Shows kode_inacbg value and type
2. Line 463-466: Code loading completion
- Shows if codes loaded successfully
- Shows parsed code array
3. Line 1534-1539: Button click handler
- Shows originalInacbgCodes state at click time
- Shows totalKlaimOriginal value
4. Line 388-395: Modal open effect
- Logs whenever isEditINACBGModalOpen changes
- Shows all relevant state values
```
**In Modal Component (edit-INACBG.tsx):**
```
1. Line 50-54: State change monitor
- Logs when selectedInacbgCodes changes
- Shows current array and length
2. Line 130-145: Modal initialization effect
- Shows currentData received from props
- Shows type and array detection
- Shows codes to be set
```
### How Logging Helps
- **Pinpoint exactly where data is lost**
- **Identify data format issues** (string vs array)
- **Verify each step of data flow**
- **Detect state initialization problems**
---
## 📋 Data Structure
### From API Response
```javascript
{
kode_inacbg: ["J75", "K45", "L20"], // Array of code strings
tipe_inacbg: "RI" | "RJ",
kelas: "Kelas 1",
total_klaim: 2776725,
// ... other fields
}
```
### In Parent Component States
```javascript
originalInacbgCodes: ["J75", "K45", "L20"] // DB snapshot (never changes)
existingInacbgCodes: ["J75", "K45", "L20"] // Calculation baseline
selectedInacbgCodes: ["J75", "K45", "L20"] // Current UI selection
```
### Passed to Modal
```javascript
currentData={{
kode_inacbg: originalInacbgCodes, // Should be array
tipe_inacbg: tipeInacbg,
kelas: kelas,
total_klaim: totalKlaimOriginal,
}}
```
### In Modal Component States
```javascript
selectedInacbgCodes: ["J75", "K45", "L20"] // To display
existingInacbgCodes: ["J75", "K45", "L20"] // For calculation baseline
```
---
## 🧪 Testing Checklist
When debugging, verify in order:
- [ ] API returns data with `kode_inacbg` field
- [ ] `kode_inacbg` is an array (not string or object)
- [ ] Array contains code strings ["J75", "K45", etc]
- [ ] `originalInacbgCodes` state updates after API fetch
- [ ] Button click logs show `originalInacbgCodes` with data
- [ ] Modal receives data in `currentData.kode_inacbg`
- [ ] Modal sets `selectedInacbgCodes` from received data
- [ ] UI renders codes list with trash icons
- [ ] Clicking trash button shows confirmation modal
- [ ] Confirming delete removes code from list
---
## 💾 Files Modified in This Session
1. **INACBG_Admin_Ruangan.tsx** (1579 lines)
- Added `originalInacbgCodes` state
- Enhanced API fetch logging
- Added button click logging
- Added modal open effect with logging
- Modified modal data passing
2. **edit-INACBG.tsx** (647 lines)
- Added `existingInacbgCodes` state
- Enhanced initialization logging
- Added state change monitoring
- Updated calculation logic
- Added delete functionality
3. **INACBG_DEBUGGING_GUIDE.md** (NEW)
- Comprehensive debugging guide
- Step-by-step troubleshooting
- Diagnostic checklist
---
## 🎯 Next Action Required
**User must run the application and check browser console (F12) to:**
1. Verify logs appear in expected sequence
2. Identify where data flow breaks
3. Share console output for analysis
4. Based on logs, apply targeted fix
Once console output is reviewed, the specific root cause will be identified and a precise fix applied.
---
## 📊 Feature Completion Matrix
| Feature | Status | Notes |
|---------|--------|-------|
| Modal UI | ✅ Complete | Fully styled and responsive |
| Open/Close | ✅ Complete | Button integration working |
| Display Empty State | ✅ Complete | Shows "Belum ada..." message |
| Add Code | ✅ Complete | Dropdown search and add working |
| Delete Code | ✅ Complete | Confirmation modal works |
| Real-time Calc | ✅ Complete | Delta calculation accurate |
| Submit to Backend | ✅ Complete | Payload structure correct |
| Load Existing Codes | 🔴 Blocked | Data not displaying |
| Edit Existing Codes | 🔴 Blocked | Depends on loading |
---
## 🚀 Known Working Scenarios
1. **User can add NEW codes** - Works perfectly
2. **User can delete NEW codes** - Works perfectly
3. **Total klaim calculation** - Accurate for new codes
4. **Modal opens/closes** - No issues
5. **Backend submission** - Correct payload structure
6. **Different patients** - Load correctly (except INACBG display)
7. **RI vs RJ selection** - Switches between code lists correctly
## ❌ Known Non-Working
1. **Existing codes NOT visible** when modal opens
2. **Cannot delete existing codes** (not visible to delete)
3. **Cannot edit existing codes** (not visible to edit)
---
## 🔗 Relationship to Other Features
- Similar implementation to **edit-pasien** modal (working reference)
- Similar calculation to **tindakan RS** fix (delta approach)
- Part of **INACBG_Admin_Ruangan** component ecosystem
- Connects to backend **/admin/billing/{id}** endpoint
- Uses **getTarifBPJSInacbgRI/RJ** data
---
## 📝 Notes
- No backend changes made (as requested)
- All frontend component created
- Comprehensive logging infrastructure in place
- Ready for debugging phase
- Design matches existing component patterns
- Responsive design tested on multiple screen sizes

View File

@@ -0,0 +1,439 @@
# 📋 PANDUAN MIGRASI MYSQL KE POSTGRESQL - STEP BY STEP
## 📌 RINGKASAN KONDISI SAAT INI
- **Backend Framework:** Golang (Gin + GORM)
- **Database Saat Ini:** MySQL (localhost:3306)
- **Database Baru:** PostgreSQL (sudah dibuat di pgAdmin4)
- **Driver Saat Ini:** `github.com/go-sql-driver/mysql` + `gorm.io/driver/mysql`
---
## ✅ TAHAP 1: PERSIAPAN & VERIFIKASI
### Step 1.1: Verifikasi Database PostgreSQL di pgAdmin4
**Lokasi file konfigurasi:**
- File: `backendcareit_v4\.env`
- File koneksi database: `backendcareit_v4\database\db.go`
**Yang perlu dicek:**
- ✓ PostgreSQL sudah berjalan dan bisa diakses dari pgAdmin4
- ✓ Database baru sudah dibuat (catat nama databasenya)
- ✓ User PostgreSQL sudah dibuat (catat username dan passwordnya)
- ✓ Port PostgreSQL (default: 5432)
- ✓ Host PostgreSQL (localhost atau IP address)
**Contoh informasi yang diperlukan:**
```
Hostname/Address: localhost
Port: 5432
Username: (nama user PostgreSQL)
Password: (password user PostgreSQL)
Database Name: (nama database PostgreSQL)
```
---
## ✅ TAHAP 2: MIGRASI DATA DARI MYSQL KE POSTGRESQL
### Step 2.1: Export Data dari MySQL
Gunakan tools berikut untuk export data:
**Option A: Menggunakan MySQL Workbench**
1. Buka MySQL Workbench
2. Koneksi ke MySQL server (localhost:3306)
3. Pilih database: `careit_db`
4. Klik menu **Server****Data Export**
5. Pilih tables yang ingin diekspor (semua tables di `careit_db`)
6. Pilih opsi **Dump Structure and Data**
7. Simpan file dengan nama: `careit_backup.sql`
**Option B: Menggunakan Command Line (PowerShell)**
```powershell
# Jalankan command ini di PowerShell
mysqldump -u root -p careit_db > "C:\Users\rengginang\Desktop\CAREIT_V4\careit_backup.sql"
# Akan diminta password MySQL (tekan Enter jika tidak ada password)
```
**Hasil yang diharapkan:**
- File SQL dengan semua struktur tabel dan data: `careit_backup.sql`
---
### Step 2.2: Konversi SQL MySQL ke PostgreSQL
Beberapa perbedaan syntax yang perlu dikonversi:
#### ⚠️ Perbedaan Utama MySQL vs PostgreSQL:
| Aspek | MySQL | PostgreSQL |
|-------|-------|-----------|
| **Type AUTO_INCREMENT** | `INT AUTO_INCREMENT` | `SERIAL` atau `INT GENERATED ALWAYS AS IDENTITY` |
| **Type DATETIME** | `DATETIME` | `TIMESTAMP` |
| **Type TINYINT** | `TINYINT(1)` | `BOOLEAN` atau `SMALLINT` |
| **Constraint Engine** | Bisa diabaikan | Harus ada (ENGINE=InnoDB menjadi tidak relevan) |
| **Charset** | `CHARACTER SET utf8mb4` | `ENCODING 'UTF8'` |
| **Function NOW()** | `NOW()` | `CURRENT_TIMESTAMP` |
#### Step 2.2a: Konversi File SQL Manual
1. Buka file `careit_backup.sql` dengan text editor (VS Code / Notepad++)
2. Lakukan replace berikut:
**Replace Pattern List:**
```
1. Ganti: `AUTO_INCREMENT` → `GENERATED ALWAYS AS IDENTITY`
Contoh:
DARI: `ID_Tarif_RS` int NOT NULL AUTO_INCREMENT,
KE: `ID_Tarif_RS` SERIAL PRIMARY KEY,
2. Ganti: `CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci` → (hapus)
Contoh:
DARI: CREATE TABLE `tarif_rs` (...) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
KE: CREATE TABLE `tarif_rs` (...);
3. Ganti: `ENGINE=InnoDB` → (hapus)
4. Ganti: `DATETIME` → `TIMESTAMP`
Contoh:
DARI: `Tanggal_Dibuat` datetime DEFAULT CURRENT_TIMESTAMP,
KE: `Tanggal_Dibuat` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
5. Ganti: backtick (`) → double quote (")
Contoh:
DARI: CREATE TABLE `users` (
KE: CREATE TABLE "users" (
6. Ganti function names:
- `NOW()` tetap bisa dipakai
- `CURDATE()` tetap bisa dipakai
```
#### Step 2.2b: Gunakan Online Tool (Opsional)
Alternatif menggunakan online converter:
- Kunjungi: https://www.pgloader.io/
- Atau gunakan tool: https://www.vertabelo.com/
---
### Step 2.3: Import Data ke PostgreSQL
**Option A: Menggunakan pgAdmin4**
1. Buka pgAdmin4 di browser (default: http://localhost:5050)
2. Login dengan credentials pgAdmin4
3. Di sebelah kiri, klik database yang sudah dibuat
4. Klik menu **Tools****Query Tool**
5. Copy isi file SQL yang sudah dikonversi
6. Paste ke Query Tool
7. Klik tombol **Execute** (atau tekan F5)
8. Tunggu sampai selesai (jika ada error, perbaiki)
**Option B: Menggunakan psql Command Line**
```powershell
# Jalankan di PowerShell
# Pastikan PostgreSQL sudah di PATH
psql -U <username> -d <database_name> -f "C:\Users\rengginang\Desktop\CAREIT_V4\careit_backup.sql"
# Contoh:
psql -U postgres -d careit_db -f "C:\Users\rengginang\Desktop\CAREIT_V4\careit_backup.sql"
```
**Hasil yang diharapkan:**
- ✓ Semua tabel sudah ter-import di PostgreSQL
- ✓ Data sudah ter-copy ke PostgreSQL
- ✓ Tidak ada error messages
---
## ✅ TAHAP 3: UPDATE BACKEND GOLANG
### Step 3.1: Update Go Modules (Dependencies)
**File yang perlu diubah:** `backendcareit_v4\go.mod`
**Perubahan:**
```
Dari: github.com/go-sql-driver/mysql v1.9.3
Dari: gorm.io/driver/mysql v1.6.0
Ke: github.com/lib/pq v1.10.9 (PostgreSQL driver)
Ke: gorm.io/driver/postgres v1.5.9 (GORM PostgreSQL driver)
```
**Di Command Line / PowerShell:**
```powershell
cd "c:\Users\rengginang\Desktop\CAREIT_V4\backendcareit_v4"
# Hapus dependency MySQL lama
go get -d -u
# Tambah PostgreSQL driver
go get github.com/lib/pq
go get gorm.io/driver/postgres
# Atau jalankan:
go get -u
go mod tidy
```
---
### Step 3.2: Update File Koneksi Database
**File yang perlu diubah:** `backendcareit_v4\database\db.go`
**Current Code (MySQL):**
```go
import (
"fmt"
"os"
_ "github.com/go-sql-driver/mysql"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
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")
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
}
```
**New Code (PostgreSQL):**
```go
import (
"fmt"
"os"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func KonekDB() (*gorm.DB, error) {
dsn := os.Getenv("DB_DSN")
if dsn == "" {
user := envOrDefault("DB_USER", "postgres")
pass := envOrDefault("DB_PASSWORD", "")
host := envOrDefault("DB_HOST", "localhost")
port := envOrDefault("DB_PORT", "5432")
name := envOrDefault("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)
}
return db, nil
}
func envOrDefault(key, fallback string) string {
val := os.Getenv(key)
if val == "" {
return fallback
}
return val
}
```
**Penjelasan Perubahan:**
- Import berubah dari `gorm.io/driver/mysql``gorm.io/driver/postgres`
- DSN format berubah dari: `user:pass@tcp(host:port)/dbname?charset=...`
- DSN format baru: `host=... port=... user=... password=... dbname=... sslmode=disable`
- Port default berubah dari 3306 → 5432
- Default user berubah dari root → postgres
---
### Step 3.3: Update File Environment Variables
**File yang perlu diubah:** `backendcareit_v4\.env`
**Current (MySQL):**
```
DB_USER=root
DB_PASSWORD=
DB_HOST=localhost
DB_PORT=3306
DB_NAME=careit_db
```
**New (PostgreSQL):**
```
DB_USER=<username_postgresql>
DB_PASSWORD=<password_postgresql>
DB_HOST=localhost
DB_PORT=5432
DB_NAME=<nama_database_postgresql>
```
**Contoh:**
```
DB_USER=postgres
DB_PASSWORD=password123
DB_HOST=localhost
DB_PORT=5432
DB_NAME=careit_db
```
---
## ✅ TAHAP 4: TESTING & VERIFIKASI
### Step 4.1: Test Koneksi Database
```powershell
cd "c:\Users\rengginang\Desktop\CAREIT_V4\backendcareit_v4"
# Jalankan backend
go run main.go
```
**Expected Output:**
```
DB_USER: postgres
DB_PASSWORD: ****
DB_HOST: localhost
DB_PORT: 5432
DB_NAME: careit_db
HOST: 0.0.0.0
PORT: 8081
Server berjalan di http://0.0.0.0:8081
Akses dari jaringan lain menggunakan IP lokal komputer + port 8081
```
✓ Jika tidak ada error, koneksi berhasil!
### Step 4.2: Test API Endpoints
Gunakan Postman atau curl untuk test:
```powershell
# Contoh test GET
curl http://localhost:8081/api/endpoint-yang-ada
# Test POST dengan data
curl -X POST http://localhost:8081/api/endpoint-yang-ada `
-H "Content-Type: application/json" `
-d '{"key":"value"}'
```
### Step 4.3: Verifikasi Data di PostgreSQL
Buka pgAdmin4 dan cek:
1. Database → careit_db → Schemas → Tables
2. Klik kanan table → View/Edit Data
3. Verifikasi bahwa data sudah ter-copy dengan benar
---
## ⚠️ TAHAP 5: HAL-HAL YANG PERLU DIPERHATIKAN
### 5.1: Case Sensitivity
- **MySQL:** Case-insensitive untuk nama table dan column
- **PostgreSQL:** Case-sensitive untuk nama table dan column
**Solusi:** Pastikan nama table dan column di models.go sesuai dengan database
### 5.2: Sequences (AUTO_INCREMENT equivalent)
PostgreSQL menggunakan SEQUENCES untuk AUTO_INCREMENT
**Jika ada issue dengan ID generation:**
```sql
-- Di PostgreSQL Query Tool, jalankan:
SELECT * FROM pg_sequences;
-- Jika sequence tidak ada, buat manual:
CREATE SEQUENCE table_name_id_seq;
```
### 5.3: Type Casting
Beberapa operasi math di Go mungkin perlu disesuaikan:
- MySQL: TINYINT(1) → Boolean
- PostgreSQL: BOOLEAN atau SMALLINT
### 5.4: Time Zone
PostgreSQL dan MySQL menangani timezone berbeda:
- Pastikan environment variable untuk timezone sudah benar
- Di DSN PostgreSQL: bisa tambah `TimeZone=Asia/Jakarta` jika perlu
---
## 📝 CHECKLIST PERSIAPAN MIGRASI
Sebelum melakukan perubahan, pastikan:
- [ ] PostgreSQL sudah terinstall dan berjalan
- [ ] Database baru sudah dibuat di PostgreSQL (catat nama, user, password, port)
- [ ] pgAdmin4 bisa mengakses PostgreSQL
- [ ] Data MySQL sudah di-backup (file .sql sudah tersimpan)
- [ ] SQL dari MySQL sudah dikonversi ke PostgreSQL format
- [ ] Data sudah ter-import ke PostgreSQL
- [ ] File `go.mod` sudah ter-update dengan PostgreSQL driver
- [ ] File `database/db.go` sudah siap untuk diubah
- [ ] File `.env` sudah siap dengan info PostgreSQL baru
---
## 📌 FILE-FILE YANG AKAN DIUBAH
1. **`backendcareit_v4\go.mod`** - Tambah/ganti dependencies
2. **`backendcareit_v4\database\db.go`** - Update koneksi database
3. **`backendcareit_v4\.env`** - Update kredensial database
**File-file yang TIDAK berubah:**
- Semua models (file di folder `models/`)
- Semua handlers (file di folder `handlers/`)
- Semua services (file di folder `services/`)
- Frontend code
---
## ⏭️ LANGKAH SELANJUTNYA
Setelah Anda:
1. ✓ Verifikasi Database PostgreSQL
2. ✓ Export data dari MySQL
3. ✓ Konversi SQL format
4. ✓ Import data ke PostgreSQL
**BARU SAAT ITU** kita akan:
1. Update `go.mod` dengan PostgreSQL driver
2. Update `database/db.go` dengan koneksi PostgreSQL
3. Update `.env` dengan kredensial PostgreSQL
4. Test seluruh aplikasi
---
## 📞 NOTES PENTING
- **Jangan ubah code dulu** sampai Anda confirm sudah siap
- **Backup data MySQL** sebelum proses migrasi
- **Test di environment lokal** dulu sebelum production
- **Dokumentasikan setiap step** untuk reference ke depan
---
**Created:** January 19, 2026
**Status:** SIAP UNTUK MIGRASI
**Last Updated:** Awaiting User Confirmation

15
backendcareit_v4/.env Normal file
View File

@@ -0,0 +1,15 @@
# DB_USER=careit_user
DB_USER=postgres
DB_PASSWORD= gakbikinkembung25
# DB_HOST=31.97.109.192
DB_HOST= localhost
DB_PORT=5432
DB_NAME=careit_db
HOST=0.0.0.0
PORT=8081
# PORT=8082
EMAIL_FROM=careit565@gmail.com
EMAIL_PASSWORD=gkhz bjax uamw xydf
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587

34
backendcareit_v4/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# Dependencies
node_modules/
package-lock.json
# Capacitor
android/
ios/
.capacitor/
# Build outputs
www/
*.apk
*.aab
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
# Go
*.exe
*.test
*.out

71
backendcareit_v4/Note.md Normal file
View File

@@ -0,0 +1,71 @@
{
"nama_dokter": "dr. Fadilah Muttaqin, Spp.A,MBiomed",
"nama_pasien": "Budi Hartono",
"jenis_kelamin": "Laki-laki",
"usia": 40,
"ruangan": "ICU",
"kelas": "1",
"tindakan_rs": [
"ASUHAN KEFARMASIAN SELAMA PERAWATAN - RAWAT INAP",
"BERCAK DARAH KERING"
],
"icd9": [
"Therapeutic ultrasound",
"Therapeutic ultrasound of vessels of head and neck"
],
"icd10": [
"Cholera",
"Cholera due to vibrio cholerae 01, biovar eltor"
],
"cara_bayar": "UMUM",
"total_tarif_rs": 250000
}
FE harus kirim gini
data untuk admin dari be:
{
"data": [
{
"nama_pasien": "mahdi Jamaludin",
"id_pasien": 1,
"Kelas": "2",
"ruangan": "R. Nusa Dua",
"total_tarif_rs": 150000,
"tindakan_rs": [
"DAR.001",
"DAR.002"
],
"icd9": [
"00.0",
"00"
],
"icd10": [
"A00",
"A00.0"
]
}
],
"status": "success"
}
if strings.TrimSpace(dokter.Password) == "" || dokter.Password != req.Password {
c.JSON(http.StatusUnauthorized, gin.H{
"status": "error",
"message": "Email atau password salah",
})
return
}
{
"dokter": {
"email": "hajengwulandari.fk@ub.ac.id",
"id": 2,
"ksm": "Anak",
"nama": "dr. Hajeng Wulandari, Sp.A, Mbiomed"
},
"status": "success",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhamVuZ3d1bGFuZGFyaS5ma0B1Yi5hYy5pZCIsImV4cCI6MTc2NDc1NzIxMCwiaWF0IjoxNzY0NjcwODEwLCJpZCI6Miwia3NtIjoiQW5hayIsIm5hbWEiOiJkci4gSGFqZW5nIFd1bGFuZGFyaSwgU3AuQSwgTWJpb21lZCJ9.X1PyxjbC1Ht3DFbvi4svqXY4hsNIS_nmYMROkRaK-Ko"
}
jadi data yang dihitung cuma yang rawat inap nanti yang isi tanggal keluar berarti admin billing dan nanti total tarif dan total klaim nanti di tampilin juga ketika datanya di tampilin sama kaya tindakan dan tarif rs nanti di admin billing juga bisa liat data tindakan lama dan icd lama dan tindakan baru dan icd bari dan inacbg lama dan inacbg baru plus total tarif yang lama di tambah yang barui dan total klaim lama nanti setelah dimasukan ditambah lagi sama total klaim baru baru dihitung billing sign baru paham gak

View File

@@ -0,0 +1,10 @@
{
"cells": [],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -0,0 +1,49 @@
package database
import (
"fmt"
"os"
_ "github.com/go-sql-driver/mysql"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
func KonekDB() (*gorm.DB, error) {
dsn := os.Getenv("DB_DSN")
if dsn == "" {
user := envOrDefault("DB_USER", "root")
pass := envOrDefault("DB_PASSWORD", "")
host := envOrDefault("DB_HOST", "localhost")
port := envOrDefault("DB_PORT", "3306")
name := envOrDefault("DB_NAME", "care_it_data")
fmt.Println("DB_USER:", os.Getenv("DB_USER"))
fmt.Println("DB_PASSWORD:", os.Getenv("DB_PASSWORD"))
fmt.Println("DB_HOST:", os.Getenv("DB_HOST"))
fmt.Println("DB_PORT:", os.Getenv("DB_PORT"))
fmt.Println("DB_NAME:", os.Getenv("DB_NAME"))
fmt.Println("HOST:", os.Getenv("HOST"))
fmt.Println("PORT:", os.Getenv("PORT"))
dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, pass, host, port, name)
}
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("gagal membuka koneksi database: %w", err)
}
return db, nil
}
func envOrDefault(key, fallback string) string {
val := os.Getenv(key)
if val == "" {
return fallback
}
return val
}

View File

@@ -0,0 +1,48 @@
package database
import (
"fmt"
"os"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func KonekPG() (*gorm.DB, error) {
dsn := os.Getenv("DB_DSN")
if dsn == "" {
user := envOrDefaultPG("DB_USER", "postgres")
pass := envOrDefaultPG("DB_PASSWORD", "gakbikinkembung25")
host := envOrDefaultPG("DB_HOST", "localhost")
port := envOrDefaultPG("DB_PORT", "5432")
name := envOrDefaultPG("DB_NAME", "careit_db")
fmt.Println("DB_USER:", os.Getenv("DB_USER"))
fmt.Println("DB_PASSWORD:", os.Getenv("DB_PASSWORD"))
fmt.Println("DB_HOST:", os.Getenv("DB_HOST"))
fmt.Println("DB_PORT:", os.Getenv("DB_PORT"))
fmt.Println("DB_NAME:", os.Getenv("DB_NAME"))
fmt.Println("HOST:", os.Getenv("HOST"))
fmt.Println("PORT:", os.Getenv("PORT"))
dsn = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", host, port, user, pass, name)
}
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("gagal membuka koneksi database: %w", err)
} else {
fmt.Println("Koneksi ke database PostgreSQL berhasil!")
}
return db, nil
}
func envOrDefaultPG(key, fallback string) string {
val := os.Getenv(key)
if val == "" {
return fallback
}
return val
}

55
backendcareit_v4/go.mod Normal file
View File

@@ -0,0 +1,55 @@
module backendcareit
go 1.23.6
require (
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.11.0
github.com/go-sql-driver/mysql v1.9.3
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/joho/godotenv v1.5.1
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)

118
backendcareit_v4/go.sum Normal file
View File

@@ -0,0 +1,118 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

View File

@@ -0,0 +1,883 @@
package handlers
import (
"encoding/json"
"errors"
"log"
"net/http"
"strconv"
"strings"
"backendcareit/database"
"backendcareit/middleware"
"backendcareit/models"
"backendcareit/services"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func RegisterRoutes(r *gin.Engine) {
// Routes get dokter
r.GET("/dokter", listDokterHandler)
// Routes get ruangan
r.GET("/ruangan", listRuanganHandler)
// Routes get icd9 icd10
r.GET("/icd10", listICD10Handler)
r.GET("/icd9", listICD9Handler)
// Health check
r.GET("/", healthHandler)
// Routes tarif
r.GET("/tarifBPJSRawatInap", listTarifBPJSRawatInapHandler)
r.GET("/tarifBPJS/:kode", detailTarifBPJSRawatInapHandler)
r.GET("/tarifBPJSRawatJalan", listTarifBPJSRawatJalanHandler)
r.GET("/tarifBPJSRawatJalan/:kode", detailTarifBPJSRawatJalanHandler)
r.GET("/tarifRS", listTarifRSHandler)
r.GET("/tarifRS/:kode", detailTarifRSHandler)
r.GET("/tarifRSByKategori/:kategori", listTarifRSByKategoriHandler)
// Routes pasien & billing
r.GET("/pasien/search", SearchPasienHandler)
r.GET("/pasien/:id", GetPasien)
r.POST("/billing", CreateBillingHandler)
r.GET("/billing/aktif", GetBillingAktifByNamaHandler)
r.PUT("/billing/:id", UpdateBillingHandler)
//close billing
r.POST("/billing/close", CloseBillingHandler)
//get all billing aktif
r.GET("/billing/aktif/all", GetAllBillingaktifhandler)
//admin edit inacbg
r.PUT("/admin/inacbg", EditINACBGAdminHandler)
// Admin: get all billing
r.GET("/admin/billing", GetAllBillingHandler)
// Admin: get riwayat billing (sudah ditutup)
r.GET("/admin/riwayat-billing", GetRiwayatBillingHandler)
// Admin: get riwayat billing with all patient data
r.GET("/admin/riwayat-pasien-all", GetRiwayatPasienAllHandler)
// Admin: get billing by ID
r.GET("/admin/billing/:id", GetBillingByIDHandler)
// Admin: post INACBG
r.POST("/admin/inacbg", PostINACBGAdminHandler)
// Admin: get ruangan dengan pasien
r.GET("/admin/ruangan-dengan-pasien", GetRuanganWithPasienHandler)
// Login dokter
r.POST("/login", LoginDokterHandler(database.DB))
// login admin
r.POST("/admin/login", LoginAdminHandler(database.DB))
}
// Coba tes koneksi dulu ya
func healthHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"message": "Server berjalan",
})
}
//Handler buat /admin/billing
func GetAllBillingHandler(c *gin.Context) {
data, err := services.GetAllBilling(database.DB)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"data": data,
})
}
// Handler buat /admin/billing/:id
func GetBillingByIDHandler(c *gin.Context) {
id := c.Param("id")
data, err := services.GetBillingByID(database.DB, id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"data": data,
})
}
// edit inacbg admin
func EditINACBGAdminHandler(c *gin.Context) {
var input models.Edit_INACBG_Request
// Ensure JSON
if c.GetHeader("Content-Type") != "application/json" {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"message": "Content-Type harus application/json",
})
return
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"message": "Data tidak valid",
"error": err.Error(),
})
return
}
if err := services.Edit_INACBG_Admin(database.DB, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengedit INACBG",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "INACBG berhasil diupdate",
})
}
// Post INACBG from admin
func PostINACBGAdminHandler(c *gin.Context) {
var input models.Post_INACBG_Admin
// Ensure JSON
if c.GetHeader("Content-Type") != "application/json" {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"message": "Content-Type harus application/json",
})
return
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"message": "Data tidak valid",
"error": err.Error(),
})
return
}
if err := services.Post_INACBG_Admin(database.DB, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal memproses INACBG",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "INACBG berhasil disimpan",
})
}
// List tarif BPJS Rawat Inap
func listTarifBPJSRawatInapHandler(c *gin.Context) {
data, err := services.GetTarifBPJSRawatInap()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengambil data",
})
return
}
c.JSON(http.StatusOK, data)
}
func detailTarifBPJSRawatInapHandler(c *gin.Context) {
kode := c.Param("kode")
data, err := services.GetTarifBPJSRawatInapByKode(kode)
if err != nil {
if services.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{
"status": "not_found",
"message": "Kode tidak ditemukan",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengambil data",
})
return
}
c.JSON(http.StatusOK, data)
}
// List tarif BPJS Rawat Jalan
func listTarifBPJSRawatJalanHandler(c *gin.Context) {
data, err := services.GetTarifBPJSRawatJalan()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengambil data",
})
return
}
c.JSON(http.StatusOK, data)
}
func detailTarifBPJSRawatJalanHandler(c *gin.Context) {
kode := c.Param("kode")
data, err := services.GetTarifBPJSRawatJalanByKode(kode)
if err != nil {
if services.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{
"status": "not_found",
"message": "Kode tidak ditemukan",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengambil data",
})
return
}
c.JSON(http.StatusOK, data)
}
// List tarif RS
func listTarifRSHandler(c *gin.Context) {
data, err := services.GetTarifRS()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengambil data",
})
return
}
c.JSON(http.StatusOK, data)
}
func detailTarifRSHandler(c *gin.Context) {
kode := c.Param("kode")
data, err := services.GetTarifRSByKode(kode)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengambil data",
})
return
}
c.JSON(http.StatusOK, data)
}
func listTarifRSByKategoriHandler(c *gin.Context) {
kategori := c.Param("kategori")
data, err := services.GetTarifRSByKategori(kategori)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengambil data",
})
return
}
c.JSON(http.StatusOK, data)
}
// ICD9
func listICD9Handler(c *gin.Context) {
data, err := services.GetICD9()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengambil data",
})
return
}
c.JSON(http.StatusOK, data)
}
// ICD10
func listICD10Handler(c *gin.Context) {
data, err := services.GetICD10()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengambil data",
})
return
}
c.JSON(http.StatusOK, data)
}
// ruangan
func listRuanganHandler(c *gin.Context) {
data, err := services.GetRuangan()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengambil data",
})
return
}
c.JSON(http.StatusOK, data)
}
// GetRuanganWithPasienHandler - Ambil ruangan yang punya pasien
func GetRuanganWithPasienHandler(c *gin.Context) {
data, err := services.GetRuanganWithPasien(database.DB)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengambil data",
})
return
}
c.JSON(http.StatusOK, data)
}
// dokter
func listDokterHandler(c *gin.Context) {
data, err := services.GetDokter()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengambil data",
})
return
}
c.JSON(http.StatusOK, data)
}
//Liat pasien sudah atau belum
func GetPasien(c *gin.Context) {
idStr := c.Param("id")
// Konversi string ke int
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(400, gin.H{
"message": "ID pasien harus berupa angka",
})
return
}
pasien, err := services.GetPasienByID(id)
if err != nil {
c.JSON(404, gin.H{
"message": "Pasien tidak ditemukan",
})
return
}
c.JSON(200, gin.H{
"message": "Data pasien ditemukan",
"data": pasien,
})
}
//add pasien baru
// CreateBillingHandler handler untuk membuat billing baru dari data frontend
func CreateBillingHandler(c *gin.Context) {
// Pastikan JSON
contentType := c.GetHeader("Content-Type")
if contentType != "application/json" {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"message": "Content-Type harus application/json",
"error": "Content-Type yang diterima: " + contentType,
})
return
}
// Gunakan map untuk menerima JSON fleksibel (bisa string atau array untuk nama_dokter)
var rawData map[string]interface{}
if err := c.ShouldBindJSON(&rawData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"message": "Data tidak valid",
"error": err.Error(),
})
return
}
// Konversi nama_dokter dari string ke array jika perlu
if namaDokterRaw, ok := rawData["nama_dokter"]; ok {
switch v := namaDokterRaw.(type) {
case string:
// Jika string, konversi ke array dengan 1 elemen
if v != "" {
rawData["nama_dokter"] = []string{v}
} else {
rawData["nama_dokter"] = []string{}
}
case []interface{}:
// Jika sudah array, konversi ke []string
namaDokterArray := make([]string, 0, len(v))
for _, item := range v {
if str, ok := item.(string); ok && str != "" {
namaDokterArray = append(namaDokterArray, str)
}
}
rawData["nama_dokter"] = namaDokterArray
case []string:
// Sudah dalam format yang benar
rawData["nama_dokter"] = v
default:
rawData["nama_dokter"] = []string{}
}
}
// Konversi map ke BillingRequest
var input models.BillingRequest
// Marshal dan unmarshal untuk konversi yang aman
jsonData, err := json.Marshal(rawData)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"message": "Gagal memproses data",
"error": err.Error(),
})
return
}
if err := json.Unmarshal(jsonData, &input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"message": "Data tidak valid",
"error": err.Error(),
})
return
}
// Panggil service → return 5 data
billing, pasien, tindakanList, icd9List, icd10List, err :=
services.DataFromFE(input)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal membuat billing",
"error": err.Error(),
})
return
}
// Response lengkap ke FE
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "Billing berhasil dibuat",
"data": gin.H{
"pasien": pasien,
"billing": billing,
"tindakan_rs": tindakanList,
"icd9": icd9List,
"icd10": icd10List,
},
})
}
// GetBillingAktifByNamaHandler - Ambil billing aktif berdasarkan nama
// Endpoint: GET /billing/aktif?nama_pasien=...
// Mengembalikan billing aktif + semua tindakan & ICD & dokter & INACBG & DPJP
func GetBillingAktifByNamaHandler(c *gin.Context) {
nama := c.Query("nama_pasien")
if strings.TrimSpace(nama) == "" {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"message": "nama_pasien wajib diisi",
})
return
}
billing, tindakan, icd9, icd10, dokter, inacbgRI, inacbgRJ, dpjp, err := services.GetBillingDetailAktifByNama(nama)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{
"status": "not_found",
"message": "Billing aktif untuk pasien tersebut tidak ditemukan",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengambil data billing",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "Billing aktif ditemukan",
"data": gin.H{
"billing": billing,
"tindakan_rs": tindakan,
"icd9": icd9,
"icd10": icd10,
"dokter": dokter,
"inacbg_ri": inacbgRI,
"inacbg_rj": inacbgRJ,
"id_dpjp": dpjp,
},
})
}
//search pasien by nama handler
func SearchPasienHandler(c *gin.Context) {
nama := c.Query("nama")
pasien, err := services.SearchPasienByNama(nama)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengambil data pasien",
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"data": pasien,
})
}
// Login dokter
func LoginDokterHandler(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
var req models.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"message": "Payload login tidak valid",
"error": err.Error(),
})
return
}
email := strings.TrimSpace(strings.ToLower(req.Email))
var dokter models.Dokter
if err := db.Where("LOWER(\"Email_UB\") = ? OR LOWER(\"Email_Pribadi\") = ?", email, email).
First(&dokter).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusUnauthorized, gin.H{
"status": "error",
"message": "Email atau password salah",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal memproses login",
"error": err.Error(),
})
return
}
// Password check — skip if password column is empty
if dokter.Password != "" && dokter.Password != req.Password {
c.JSON(http.StatusUnauthorized, gin.H{
"status": "error",
"message": "Email atau password salah",
})
return
}
token, err := middleware.GenerateToken(dokter, email)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal membuat token",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"token": token,
"dokter": gin.H{
"id": dokter.ID_Dokter,
"nama": dokter.Nama_Dokter,
"ksm": dokter.KSM,
"email": email,
},
})
}
}
// SendEmailTestHandler handler untuk test email
func SendEmailTestHandler(c *gin.Context) {
if err := services.SendEmailTest(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal mengirim email test",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "Email test berhasil dikirim ke stylohype685@gmail.com dan pasaribumonica2@gmail.com",
})
}
// SendEmailCustomHandler - kirim email tes ke daftar penerima yang diberikan
func SendEmailCustomHandler(c *gin.Context) {
var req struct {
To []string `json:"to" binding:"required,min=1"`
Subject string `json:"subject"`
Body string `json:"body"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "message": "Request harus JSON dengan field 'to' sebagai array email"})
return
}
subject := req.Subject
if strings.TrimSpace(subject) == "" {
subject = "Test Email - Sistem Billing Care IT"
}
body := req.Body
if strings.TrimSpace(body) == "" {
body = "<p>Ini adalah email test dari sistem billing Care IT.</p>"
}
if err := services.SendEmailToMultiple(req.To, subject, body); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Gagal mengirim email", "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "success", "message": "Email test berhasil dikirim"})
}
func LoginAdminHandler(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
var req struct {
Nama_Admin string `json:"Nama_Admin" binding:"required"`
Password string `json:"Password" binding:"required"`
}
// Bind & validate
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Nama_Admin dan Password harus diisi",
})
return
}
// Trim dan normalize input
namaAdmin := strings.TrimSpace(req.Nama_Admin)
if namaAdmin == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Nama_Admin tidak boleh kosong",
})
return
}
// Query admin_ruangan dengan case-insensitive
var admin models.Admin_Ruangan //Admin_Ruangan
if err := db.Where("LOWER(\"Nama_Admin\") = ?", strings.ToLower(namaAdmin)).First(&admin).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Admin tidak ditemukan",
})
return
}
// Check password
if admin.Password != req.Password {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Password salah",
})
return
}
// Generate token & return
token, err := middleware.GenerateTokenAdmin(admin, req.Nama_Admin)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Gagal membuat token",
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"token": token,
"admin": gin.H{
"id": admin.ID_Admin,
"nama_admin": admin.Nama_Admin,
"id_ruangan": admin.ID_Ruangan,
},
})
}
}
// UpdateBillingHandler - update identitas pasien dalam billing
func UpdateBillingHandler(c *gin.Context) {
idStr := c.Param("id")
billingId, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"message": "ID billing tidak valid",
"error": err.Error(),
})
return
}
// Parse request body
var updateReq struct {
Nama_Pasien string `json:"nama_pasien"`
Usia int `json:"usia"`
Jenis_Kelamin string `json:"jenis_kelamin"`
Ruangan string `json:"ruangan"`
Kelas string `json:"kelas"`
Tindakan_Rs []string `json:"tindakan_rs"`
ICD9 []string `json:"icd9"`
ICD10 []string `json:"icd10"`
Billing_sign *string `json:"billing_sign"` // Optional: jika dikirimkan akan diupdate
Total_Tarif_RS *float64 `json:"total_tarif_rs"` // Optional: jika dikirimkan akan diupdate
}
if err := c.ShouldBindJSON(&updateReq); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"message": "Data tidak valid",
"error": err.Error(),
})
return
}
// Panggil service untuk update dengan lookup kode
if err := services.EditPasienComplete(billingId, updateReq.Nama_Pasien, updateReq.Usia, updateReq.Jenis_Kelamin, updateReq.Ruangan, updateReq.Kelas, updateReq.Tindakan_Rs, updateReq.ICD9, updateReq.ICD10, updateReq.Billing_sign, updateReq.Total_Tarif_RS); err != nil {
log.Printf("[EDIT_HANDLER] ERROR - Service returned error: %v\n", err)
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal update data billing",
"error": err.Error(),
"details": err.Error(), // Add details untuk debugging
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "Data billing berhasil diupdate",
})
}
// CloseBillingHandler - handler untuk menutup billing
func CloseBillingHandler(c *gin.Context) {
var closeReq models.Close_billing
// Pastikan JSON
if c.GetHeader("Content-Type") != "application/json" {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"message": "Content-Type harus application/json",
})
return
}
if err := c.ShouldBindJSON(&closeReq); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"status": "error",
"message": "Data tidak valid",
"error": err.Error(),
})
return
}
if err := services.CloseBilling(closeReq); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": "Gagal menutup billing",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "Billing berhasil ditutup",
})
}
// GetRiwayatBillingHandler - Handler buat ngambil riwayat billing yang udah ditutup
func GetRiwayatBillingHandler(c *gin.Context) {
data, err := services.GetAllRiwayatpasien(database.DB)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"data": data,
})
}
func GetAllBillingaktifhandler(c *gin.Context) {
data, err := services.GetAllBillingaktif(database.DB)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"data": data,
})
}
// GetRiwayatPasienAllHandler - Handler buat ngambil riwayat pasien lengkap
func GetRiwayatPasienAllHandler(c *gin.Context) {
data, err := services.GetRiwayatPasienAll(database.DB)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"data": data,
})
}

54
backendcareit_v4/main.go Normal file
View File

@@ -0,0 +1,54 @@
package main
import (
"fmt"
"log"
"os"
"backendcareit/database"
"backendcareit/handlers"
"github.com/joho/godotenv"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
_ = godotenv.Load()
db, err := database.KonekPG()
if err != nil {
log.Fatal("Gagal koneksi database:", err)
}
database.DB = db
r := gin.Default()
config := cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
AllowCredentials: true,
}
r.Use(cors.New(config))
handlers.RegisterRoutes(r)
host := os.Getenv("HOST")
if host == "" {
host = "0.0.0.0"
}
port := os.Getenv("PORT")
if port == "" {
port = "8081"
}
listenAddr := fmt.Sprintf("%s:%s", host, port)
fmt.Printf("Server berjalan di http://%s\n", listenAddr)
fmt.Println("Akses dari jaringan lain menggunakan IP lokal komputer + port", port)
if err := r.Run(listenAddr); err != nil {
log.Fatal("Gagal menjalankan server:", err)
}
}

View File

@@ -0,0 +1,104 @@
package middleware
import (
"errors"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
"backendcareit/models"
)
// Payload untuk login dokter - berisi credensial yang diperlukan
var jwtSecret = []byte(getJWTSecret())
func getJWTSecret() string {
if secret := os.Getenv("JWT_SECRET"); secret != "" {
return secret
}
return "SECRET_KAMU"
}
// LoginDokterHandler - Ini handler POST /login yang ngecek kredensial dokter
// Kalau cocok, langsung kasih JWT ke dia
// GenerateToken - Bikin JWT buat dokter, berlaku 24 jam terus expired hehe
func GenerateToken(dokter models.Dokter, email string) (string, error) {
claims := jwt.MapClaims{
"id": dokter.ID_Dokter,
"nama": dokter.Nama_Dokter,
"ksm": dokter.KSM,
"email": email,
"exp": time.Now().Add(24 * time.Hour).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
// GenerateTokenAdmin - Serupa dengan dokter, tapi ini buat admin dengan role khusus
func GenerateTokenAdmin(admin models.Admin_Ruangan, namaAdmin string) (string, error) {
claims := jwt.MapClaims{
"id": admin.ID_Admin,
"nama_admin": admin.Nama_Admin,
"id_ruangan": admin.ID_Ruangan,
"role": "admin",
"exp": time.Now().Add(24 * time.Hour).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
// AuthMiddleware - Middleware pengecekan token JWT
// Ngecek header Authorization, parse tokennya, terus simpan data di context jika valid
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"status": "error",
"message": "Authorization header wajib menggunakan Bearer token",
})
return
}
tokenString := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer"))
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("metode tanda tangan tidak dikenal")
}
return jwtSecret, nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"status": "error",
"message": "Token tidak valid atau kadaluarsa",
})
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"status": "error",
"message": "Token tidak valid",
})
return
}
c.Set("dokter_id", claims["id"])
c.Set("dokter_nama", claims["nama"])
c.Set("dokter_ksm", claims["ksm"])
c.Set("dokter_email", claims["email"])
c.Next()
}
}

View File

@@ -0,0 +1,352 @@
package models
import "time"
// Tarif Models - Data struktur buat tarif dari berbagai sumber
type TarifBPJSRawatInap struct {
KodeINA string `gorm:"column:ID_INACBG_RI"`
Deskripsi string `gorm:"column:Tindakan_RI"`
Kelas1 float64 `gorm:"column:Tarif_Kelas_1"`
Kelas2 float64 `gorm:"column:Tarif_Kelas_2"`
Kelas3 float64 `gorm:"column:Tarif_Kelas_3"`
}
type TarifBPJSRawatJalan struct {
KodeINA string `gorm:"column:ID_INACBG_RJ"`
Deskripsi string `gorm:"column:Tindakan_RJ"`
TarifINACBG float64 `gorm:"column:Tarif_RJ" json:"tarif_inacbg"`
}
type TarifRS struct {
KodeRS string `gorm:"column:ID_Tarif_RS"`
Deskripsi string `gorm:"column:Tindakan_RS"`
Harga int `gorm:"column:Tarif_RS"`
Kategori string `gorm:"column:Kategori_RS"`
}
func (TarifBPJSRawatJalan) TableName() string {
return "ina_cbg_rawatjalan"
}
func (TarifBPJSRawatInap) TableName() string {
return "ina_cbg_rawatinap"
}
func (TarifRS) TableName() string {
return "tarif_rs"
}
// billing_inacbg_RI
type Billing_INACBG_RI struct {
ID_Billing int `gorm:"column:ID_Billing"`
Kode_INACBG string `gorm:"column:ID_INACBG_RI"`
}
func (Billing_INACBG_RI) TableName() string {
return "billing_inacbg_ri"
}
// billing_inacbg_RJ
type Billing_INACBG_RJ struct {
ID_Billing int `gorm:"column:ID_Billing"`
Kode_INACBG string `gorm:"column:ID_INACBG_RJ"`
}
func (Billing_INACBG_RJ) TableName() string {
return "billing_inacbg_rj"
}
type Billing_DPJP struct {
ID_Billing int `gorm:"column:ID_Billing;primaryKey"`
ID_DPJP int `gorm:"column:ID_DPJP;primaryKey"`
}
func (Billing_DPJP) TableName() string {
return "billing_dpjp"
}
// ICD9
type ICD9 struct {
Kode_ICD9 string `gorm:"column:ID_ICD9"`
Prosedur string `gorm:"column:Prosedur"`
Versi string `gorm:"column:Versi_ICD9"`
}
func (ICD9) TableName() string {
return "icd9"
}
// ICD10
type ICD10 struct {
Kode_ICD10 string `gorm:"column:ID_ICD10"`
Diagnosa string `gorm:"column:Diagnosa"`
Versi string `gorm:"column:Versi_ICD10"`
}
func (ICD10) TableName() string {
return "icd10"
}
// ruangan
type Ruangan struct {
ID_Ruangan string `gorm:"column:ID_Ruangan"`
Jenis_Ruangan string `gorm:"column:Jenis_Ruangan"`
Nama_Ruangan string `gorm:"column:Nama_Ruangan"`
Keterangan string `gorm:"column:keterangan"`
Kategori_ruangan string `gorm:"column:kategori_ruangan"`
}
func (Ruangan) TableName() string {
return "ruangan"
}
// dokter
type Dokter struct {
ID_Dokter int `gorm:"column:ID_Dokter;primaryKey"`
Nama_Dokter string `gorm:"column:Nama_Dokter"`
Password string `gorm:"column:Password"`
Status string `gorm:"column:Status"`
KSM string `gorm:"column:KSM"`
Email_UB string `gorm:"column:Email_UB"`
Email_Pribadi string `gorm:"column:Email_Pribadi"`
}
func (Dokter) TableName() string {
return "dokter"
}
// PASIEN
type Pasien struct {
ID_Pasien int `gorm:"column:ID_Pasien;primaryKey;autoIncrement"`
Nama_Pasien string `gorm:"column:Nama_Pasien"`
Jenis_Kelamin string `gorm:"column:Jenis_Kelamin"`
Usia int `gorm:"column:Usia"`
Ruangan string `gorm:"column:Ruangan"`
Kelas string `gorm:"column:Kelas"`
}
type Kelas string
const (
Kelas_1 Kelas = "1"
Kelas_2 Kelas = "2"
Kelas_3 Kelas = "3"
)
type Jenis_kelamin string
const (
Jenis_Kelamin_Laki_laki Jenis_kelamin = "Laki-laki"
Jenis_Kelamin_Perempuan Jenis_kelamin = "Perempuan"
)
func (Pasien) TableName() string {
return "pasien"
}
// login dokter
type LoginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
//edit inacbg admin
type Edit_INACBG_Request struct {
ID_Billing int `json:"id_billing"`
Tipe_inacbg string `json:"tipe_inacbg"`
Kode_inacbg []string `json:"kode_inacbg"`
Kode_Delete []string `json:"kode_delete"`
Total_Klaim float64 `json:"total_klaim"`
Billing_Sign string `json:"billing_sign"`
}
//close billing
type Close_billing struct {
ID_Billing int `json:"id_billing"`
Tanggal_Keluar string `json:"tanggal_keluar"`
}
//riwayat billing pasien
type Riwayat_Pasien_all struct {
ID_Billing int `json:"id_billing"`
ID_Pasien int `json:"id_pasien"`
Nama_Pasien string `json:"nama_pasien"`
Jenis_Kelamin string `json:"jenis_kelamin"`
Usia int `json:"usia"`
Ruangan string `json:"ruangan"`
Nama_Ruangan string `json:"nama_ruangan"` // ← Added: Ruangan name for display
Kelas string `json:"kelas"`
ID_DPJP int `json:"id_dpjp"` // ← Added: Doctor in charge
Nama_DPJP string `json:"nama_dpjp"`
Tanggal_Keluar string `json:"tanggal_keluar"`
Tanggal_Masuk string `json:"tanggal_masuk"` // Tanggal_Masuk *time.Time `json:"tanggal_masuk"`
Tanggal_Tindakan *time.Time `json:"tanggal_tindakan"`
Tindakan_RS []string `json:"tindakan_rs"`
ICD9 []string `json:"icd9"`
ICD10 []string `json:"icd10"`
Kode_INACBG string `json:"kode_inacbg"`
Total_Tarif_RS float64 `json:"total_tarif_rs"` // ← Added: Hospital tariff
Total_Klaim float64 `json:"total_klaim"` // ← Added: BPJS claim
}
//billing pasien
type BillingPasien struct {
ID_Billing int `gorm:"column:ID_Billing;primaryKey;autoIncrement" json:"id_billing"`
ID_Pasien int `gorm:"column:ID_Pasien" json:"id_pasien"`
Cara_Bayar string `gorm:"column:Cara_Bayar" json:"cara_bayar"`
Tanggal_masuk *time.Time `gorm:"column:Tanggal_Masuk" json:"tanggal_masuk"`
Tanggal_keluar *time.Time `gorm:"column:Tanggal_Keluar" json:"tanggal_keluar"`
Total_Tarif_RS float64 `gorm:"column:Total_Tarif_RS" json:"total_tarif_rs"`
Total_Klaim float64 `gorm:"column:Total_Klaim" json:"total_klaim"`
Billing_sign string `gorm:"column:Billing_Sign" json:"billing_sign"`
}
type Cara_bayar string
const (
Cara_Bayar_BPJS Cara_bayar = "BPJS"
Cara_Bayar_UMUM Cara_bayar = "UMUM"
)
func (BillingPasien) TableName() string {
return "billing_pasien"
}
// BillingRequest untuk menerima data dari frontend
type BillingRequest struct {
Nama_Dokter []string `json:"nama_dokter" binding:"required"` // Array untuk multiple doctors
Nama_Pasien string `json:"nama_pasien" binding:"required"`
Jenis_Kelamin string `json:"jenis_kelamin" binding:"required"`
Usia int `json:"usia" binding:"required"`
ID_DPJP int `json:"id_dpjp"`
Ruangan string `json:"ruangan" binding:"required"`
Kelas string `json:"kelas" binding:"required"`
Tindakan_RS []string `json:"tindakan_rs" binding:"required"`
Tanggal_Keluar string `json:"tanggal_keluar"`
Billing_sign string `json:"billing_sign"`
ICD9 []string `json:"icd9"`
ICD10 []string `json:"icd10" binding:"required"`
Cara_Bayar string `json:"cara_bayar" binding:"required"`
Total_Tarif_RS float64 `json:"total_tarif_rs"`
Total_Klaim_BPJS float64 `json:"total_klaim_bpjs"` // ← Added: Baseline BPJS claim from FE
}
// admin ruangan //Admin_Ruangan
type Admin_Ruangan struct {
ID_Admin int `gorm:"column:ID_Admin"`
Nama_Admin string `gorm:"column:Nama_Admin"`
Password string `gorm:"column:Password"`
ID_Ruangan string `gorm:"column:ID_Ruangan"`
}
func (Admin_Ruangan) TableName() string {
return "admin_ruangan"
}
// billing_Tidakan
type Billing_Tindakan struct {
ID_Billing int `gorm:"column:ID_Billing;primaryKey;not null"`
ID_Tarif_RS string `gorm:"column:ID_Tarif_RS;primaryKey;not null"`
Tanggal_Tindakan *time.Time `gorm:"column:tanggal_tindakan"`
}
func (Billing_Tindakan) TableName() string {
return "billing_tindakan"
}
// billing_ICD9 dan ICD10
type Billing_ICD9 struct {
ID_Billing int `gorm:"column:ID_Billing;primaryKey;not null"`
ID_ICD9 string `gorm:"column:ID_ICD9;primaryKey;not null"`
}
type Billing_ICD10 struct {
ID_Billing int `gorm:"column:ID_Billing;primaryKey;not null"`
ID_ICD10 string `gorm:"column:ID_ICD10;primaryKey;not null"`
}
func (Billing_ICD9) TableName() string {
return "billing_icd9"
}
func (Billing_ICD10) TableName() string {
return "billing_icd10"
}
// billing_Dokter - relasi many-to-many antara billing dan dokter dengan tracking tanggal
type Billing_Dokter struct {
ID_Billing int `gorm:"column:ID_Billing"`
ID_Dokter int `gorm:"column:ID_Dokter"`
Tanggal *time.Time `gorm:"column:tanggal"` // Tanggal kapan dokter menangani pasien
}
func (Billing_Dokter) TableName() string {
return "billing_dokter"
}
// riwayat pasien
type Riwayat_Pasien struct {
ID_Billing int `json:"id_billing"`
Nama_pasien string `json:"nama_pasien"`
ID_Pasien int `json:"id_pasien"`
Kelas string `json:"Kelas"`
Ruangan string `json:"ruangan"`
Total_Tarif_RS float64 `json:"total_tarif_rs"`
Total_Klaim float64 `json:"total_klaim"`
Tindakan_RS []string `json:"tindakan_rs"`
ICD9 []string `json:"icd9"`
ICD10 []string `json:"icd10"`
INACBG_RI []string `json:"inacbg_ri"`
INACBG_RJ []string `json:"inacbg_rj"`
Billing_sign string `json:"billing_sign"`
Nama_Dokter []string `json:"nama_dokter"`
}
// Request untuk tampilan data Admin ( pengisian inacbg)
type Request_Admin_Inacbg struct {
ID_Billing int `json:"id_billing"`
Nama_pasien string `json:"nama_pasien"`
ID_Pasien int `json:"id_pasien"`
Kelas string `json:"Kelas"`
Ruangan string `json:"ruangan"`
Total_Tarif_RS float64 `json:"total_tarif_rs"`
Total_Klaim float64 `json:"total_klaim"`
ID_DPJP int `json:"id_dpjp"`
Tindakan_RS []string `json:"tindakan_rs"`
ICD9 []string `json:"icd9"`
ICD10 []string `json:"icd10"`
INACBG_RI []string `json:"inacbg_ri"`
INACBG_RJ []string `json:"inacbg_rj"`
Billing_sign string `json:"billing_sign"`
Nama_Dokter []string `json:"nama_dokter"`
}
// post ke data base
type Post_INACBG_Admin struct {
ID_Billing int `json:"id_billing"`
Tipe_inacbg string `json:"tipe_inacbg"`
Kode_INACBG []string `json:"kode_inacbg"`
Total_klaim float64 `json:"total_klaim"`
Billing_sign string `json:"billing_sign"`
Tanggal_keluar string `json:"tanggal_keluar"` // Diisi oleh admin billing
}
// login dokter
type loginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
func (loginRequest) TableName() string {
return "dokter"
}
// getpasienwithallicd9andicd10,andtindakanrs

View File

@@ -0,0 +1,87 @@
package scripts
import (
"fmt"
"log"
"backendcareit/database"
)
func CheckAdmin() {
// Nyambungin ke database
db, err := database.KonekDB()
if err != nil {
log.Fatalf("Gagal koneksi database: %v", err)
}
// Set koneksi database
database.DB = db
// Cek data admin
type AdminRuangan struct {
ID_Admin int `gorm:"column:ID_Admin"`
Nama_Admin string `gorm:"column:Nama_Admin"`
Password string `gorm:"column:Password"`
ID_Ruangan *int `gorm:"column:ID_Ruangan"`
}
var admins []AdminRuangan
// Ambil semua admin
result := db.Table("admin_ruangan").Find(&admins)
if result.Error != nil {
log.Fatalf("Gagal query admin: %v", result.Error)
}
fmt.Printf("Total admin ditemukan: %d\n\n", len(admins))
if len(admins) == 0 {
fmt.Println("⚠️ Tidak ada data admin di database!")
fmt.Println("\nJalankan script insert_admin.go untuk menambahkan data admin:")
fmt.Println(" go run scripts/insert_admin.go")
return
}
// Tampilin semua admin
for i, admin := range admins {
fmt.Printf("Admin #%d:\n", i+1)
fmt.Printf(" ID_Admin: %d\n", admin.ID_Admin)
fmt.Printf(" Nama_Admin: '%s'\n", admin.Nama_Admin)
fmt.Printf(" Password: '%s' (length: %d)\n", admin.Password, len(admin.Password))
if admin.ID_Ruangan != nil {
fmt.Printf(" ID_Ruangan: %d\n", *admin.ID_Ruangan)
} else {
fmt.Printf(" ID_Ruangan: NULL\n")
}
fmt.Println()
}
// Test query buat username 'admin'
var admin AdminRuangan
err = db.Table("admin_ruangan").
Where("Nama_Admin = ?", "admin").
First(&admin).Error
if err != nil {
fmt.Println("❌ Query dengan Nama_Admin = 'admin' GAGAL")
fmt.Printf(" Error: %v\n", err)
} else {
fmt.Println("✅ Query dengan Nama_Admin = 'admin' BERHASIL")
fmt.Printf(" ID_Admin: %d\n", admin.ID_Admin)
fmt.Printf(" Nama_Admin: '%s'\n", admin.Nama_Admin)
fmt.Printf(" Password: '%s'\n", admin.Password)
}
// Test query case-insensitive
err = db.Table("admin_ruangan").
Where("LOWER(Nama_Admin) = LOWER(?)", "admin").
First(&admin).Error
if err != nil {
fmt.Println("❌ Query case-insensitive GAGAL")
fmt.Printf(" Error: %v\n", err)
} else {
fmt.Println("✅ Query case-insensitive BERHASIL")
fmt.Printf(" ID_Admin: %d\n", admin.ID_Admin)
fmt.Printf(" Nama_Admin: '%s'\n", admin.Nama_Admin)
}
}

View File

@@ -0,0 +1,47 @@
package scripts
import (
"fmt"
"log"
"backendcareit/database"
)
func main() {
// Nyambungin ke database
db, err := database.KonekDB()
if err != nil {
log.Fatalf("Gagal koneksi database: %v", err)
}
// Set koneksi database
database.DB = db
// Cek admin udah ada atau belum
var count int64
db.Table("admin_ruangan").Where("Nama_Admin = ?", "admin").Count(&count)
if count > 0 {
fmt.Println("Admin dengan username 'admin' sudah ada di database.")
fmt.Println("Ngapus admin yang lama...")
db.Table("admin_ruangan").Where("Nama_Admin = ?", "admin").Delete(nil)
}
// Masukin admin yang baru
result := db.Exec(`
INSERT INTO admin_ruangan (Nama_Admin, Password, ID_Ruangan)
VALUES (?, ?, ?)
`, "admin", "admin123", nil)
if result.Error != nil {
log.Fatalf("Gagal insert admin: %v", result.Error)
}
if result.RowsAffected > 0 {
fmt.Println("✓ Data admin berhasil ditambahkan!")
fmt.Println(" Username: admin")
fmt.Println(" Password: admin123")
} else {
fmt.Println("Tidak ada data yang ditambahkan.")
}
}

View File

@@ -0,0 +1,96 @@
package services
import (
"backendcareit/models"
"fmt"
"log"
"gorm.io/gorm"
)
func Edit_INACBG_Admin(db *gorm.DB, input models.Edit_INACBG_Request) error {
log.Printf("[Edit INACBG] Received ID_Billing=%d, Tipe=%s, Kode_count=%d, Delete_count=%d, Total_Klaim=%.2f, Billing_Sign=%s\n",
input.ID_Billing, input.Tipe_inacbg, len(input.Kode_inacbg), len(input.Kode_Delete), input.Total_Klaim, input.Billing_Sign)
tx := db.Begin()
if tx.Error != nil {
return tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 1. Hapus kode yang udah dipilih untuk dihapus
if len(input.Kode_Delete) > 0 {
switch input.Tipe_inacbg {
case "RI":
if err := tx.Where("\"ID_Billing\" = ? AND \"ID_INACBG_RI\" IN ?", input.ID_Billing, input.Kode_Delete).Delete(&models.Billing_INACBG_RI{}).Error; err != nil {
tx.Rollback()
return fmt.Errorf("gagal delete INACBG RI: %w", err)
}
case "RJ":
if err := tx.Where("\"ID_Billing\" = ? AND \"ID_INACBG_RJ\" IN ?", input.ID_Billing, input.Kode_Delete).Delete(&models.Billing_INACBG_RJ{}).Error; err != nil {
tx.Rollback()
return fmt.Errorf("gagal delete INACBG RJ: %w", err)
}
default:
tx.Rollback()
return fmt.Errorf("invalid tipe_inacbg: %s", input.Tipe_inacbg)
}
}
// 2. Tambahin kode INACBG yang baru
if len(input.Kode_inacbg) > 0 {
switch input.Tipe_inacbg {
case "RI":
for _, kode := range input.Kode_inacbg {
inacbgRI := models.Billing_INACBG_RI{
ID_Billing: input.ID_Billing,
Kode_INACBG: kode,
}
if err := tx.Create(&inacbgRI).Error; err != nil {
tx.Rollback()
return fmt.Errorf("gagal insert INACBG RI kode %s: %w", kode, err)
}
}
case "RJ":
for _, kode := range input.Kode_inacbg {
inacbgRJ := models.Billing_INACBG_RJ{
ID_Billing: input.ID_Billing,
Kode_INACBG: kode,
}
if err := tx.Create(&inacbgRJ).Error; err != nil {
tx.Rollback()
return fmt.Errorf("gagal insert INACBG RJ kode %s: %w", kode, err)
}
}
}
}
// 3. Update data di tabel billing_pasien
updateData := map[string]interface{}{
"Total_Klaim": input.Total_Klaim,
"Billing_Sign": input.Billing_Sign,
}
if err := tx.Model(&models.BillingPasien{}).Where("\"ID_Billing\" = ?", input.ID_Billing).Updates(updateData).Error; err != nil {
tx.Rollback()
return fmt.Errorf("gagal update billing_pasien: %w", err)
}
if err := tx.Commit().Error; err != nil {
return err
}
// 4. Ngirim email kalo billing_sign gak kosong
go func(id int) {
if err := SendEmailBillingSignToDokter(id); err != nil {
log.Printf("Warning: Gagal mengirim email ke dokter untuk billing ID %d: %v\n", id, err)
}
}(input.ID_Billing)
return nil
}

View File

@@ -0,0 +1,173 @@
package services
import (
"errors"
"fmt"
"log"
"time"
"backendcareit/database"
"backendcareit/models"
"gorm.io/gorm"
)
// EditPasienComplete - Update data identitas pasien dalam billing (nama, umur, ruangan, dll) terus dengan lookup kode
func EditPasienComplete(billingId int, namaPasien string, usia int, jeniKelamin string, ruangan string, kelas string, tindakan []string, icd9 []string, icd10 []string, billingSign *string, totalTarifRS *float64) error {
log.Printf("[EditPasien] START - billingId:%d, nama:%s, tindakan_count:%d, icd9_count:%d, icd10_count:%d\n", billingId, namaPasien, len(tindakan), len(icd9), len(icd10))
// Get billing
var billing models.BillingPasien
if err := database.DB.Where("\"ID_Billing\" = ?", billingId).First(&billing).Error; err != nil {
log.Printf("[EditPasien] ERROR - billing not found: %v\n", err)
return errors.New("billing tidak ditemukan")
}
log.Printf("[EditPasien] ✓ Billing found - ID_Pasien: %d\n", billing.ID_Pasien)
// Start transaction
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// Update pasien data
if err := tx.Model(&models.Pasien{}).
Where("\"ID_Pasien\" = ?", billing.ID_Pasien).
Updates(map[string]interface{}{
"Nama_Pasien": namaPasien,
"Usia": usia,
"Jenis_Kelamin": jeniKelamin,
"Ruangan": ruangan,
"Kelas": kelas,
}).Error; err != nil {
tx.Rollback()
return errors.New("gagal update data pasien: " + err.Error())
}
// Delete existing tindakan
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_Tindakan{}).Error; err != nil {
tx.Rollback()
return errors.New("gagal delete tindakan: " + err.Error())
}
// Insert new tindakan dengan lookup berdasarkan nama tindakan
now := time.Now()
for _, tindakanNama := range tindakan {
if tindakanNama != "" {
log.Printf("[EditPasien] Looking up tindakan: '%s'\n", tindakanNama)
// Lookup tarif by deskripsi (nama tindakan) - use quoted column name for PostgreSQL
var tarif models.TarifRS
if err := tx.Where("\"Tindakan_RS\" = ?", tindakanNama).First(&tarif).Error; err != nil {
log.Printf("[EditPasien] ERROR - tindakan lookup failed: %v\n", err)
if errors.Is(err, gorm.ErrRecordNotFound) {
tx.Rollback()
log.Printf("[EditPasien] ERROR - tindakan '%s' not found in tarif_rs\n", tindakanNama)
return fmt.Errorf("tindakan '%s' tidak ditemukan", tindakanNama)
}
tx.Rollback()
return errors.New("gagal lookup tindakan: " + err.Error())
}
log.Printf("[EditPasien] ✓ Tindakan found - ID: %s, Harga: %d\n", tarif.KodeRS, tarif.Harga)
newTindakan := models.Billing_Tindakan{
ID_Billing: billingId,
ID_Tarif_RS: tarif.KodeRS,
Tanggal_Tindakan: &now,
}
if err := tx.Create(&newTindakan).Error; err != nil {
tx.Rollback()
return errors.New("gagal insert tindakan: " + err.Error())
}
}
}
// Delete existing ICD9
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_ICD9{}).Error; err != nil {
tx.Rollback()
return errors.New("gagal delete ICD9: " + err.Error())
}
// Insert new ICD9 dengan lookup berdasarkan nama prosedur
for _, icd9Nama := range icd9 {
if icd9Nama != "" {
// Lookup ICD9 by prosedur name
var icd9Data models.ICD9
if err := tx.Where("\"Prosedur\" = ?", icd9Nama).First(&icd9Data).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
tx.Rollback()
return fmt.Errorf("ICD9 '%s' tidak ditemukan", icd9Nama)
}
tx.Rollback()
return errors.New("gagal lookup ICD9: " + err.Error())
}
newICD9 := models.Billing_ICD9{
ID_Billing: billingId,
ID_ICD9: icd9Data.Kode_ICD9,
}
if err := tx.Create(&newICD9).Error; err != nil {
tx.Rollback()
return errors.New("gagal insert ICD9: " + err.Error())
}
}
}
// Delete existing ICD10
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_ICD10{}).Error; err != nil {
tx.Rollback()
return errors.New("gagal delete ICD10: " + err.Error())
}
// Insert new ICD10 dengan lookup berdasarkan nama diagnosa
for _, icd10Nama := range icd10 {
if icd10Nama != "" {
// Lookup ICD10 by diagnosa name
var icd10Data models.ICD10
if err := tx.Where("\"Diagnosa\" = ?", icd10Nama).First(&icd10Data).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
tx.Rollback()
return fmt.Errorf("ICD10 '%s' tidak ditemukan", icd10Nama)
}
tx.Rollback()
return errors.New("gagal lookup ICD10: " + err.Error())
}
newICD10 := models.Billing_ICD10{
ID_Billing: billingId,
ID_ICD10: icd10Data.Kode_ICD10,
}
if err := tx.Create(&newICD10).Error; err != nil {
tx.Rollback()
return errors.New("gagal insert ICD10: " + err.Error())
}
}
}
// Update billing_sign jika dikirimkan dari FE
if billingSign != nil {
if err := tx.Model(&models.BillingPasien{}).
Where("\"ID_Billing\" = ?", billingId).
Update("Billing_Sign", *billingSign).Error; err != nil {
tx.Rollback()
return errors.New("gagal update billing_sign: " + err.Error())
}
}
// Update total_tarif_rs jika dikirimkan dari FE
if totalTarifRS != nil {
if err := tx.Model(&models.BillingPasien{}).
Where("\"ID_Billing\" = ?", billingId).
Update("Total_Tarif_RS", *totalTarifRS).Error; err != nil {
tx.Rollback()
return errors.New("gagal update total_tarif_rs: " + err.Error())
}
}
// Commit transaction
if err := tx.Commit().Error; err != nil {
return errors.New("gagal commit transaction: " + err.Error())
}
return nil
}

View File

@@ -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
}

View File

@@ -0,0 +1,284 @@
package services
import (
"errors"
"fmt"
"net/smtp"
"os"
"strings"
"backendcareit/database"
"backendcareit/models"
"gorm.io/gorm"
)
// SendEmail - Ngirim email pake SMTP bro
func SendEmail(to, subject, body string) error {
// Ambil konfigurasi dari env variable dulu, lebih aman
from := os.Getenv("EMAIL_FROM")
password := os.Getenv("EMAIL_PASSWORD")
smtpHost := os.Getenv("SMTP_HOST")
smtpPort := os.Getenv("SMTP_PORT")
// Kalau env variable gak ada, pake default value (biar kompatibel sama versi lama)
if from == "" {
from = "careit565@gmail.com"
}
if password == "" {
password = "gkhz bjax uamw xydf"
}
if smtpHost == "" {
smtpHost = "smtp.gmail.com"
}
if smtpPort == "" {
smtpPort = "587"
}
if from == "" || password == "" || smtpHost == "" || smtpPort == "" {
return fmt.Errorf("konfigurasi email tidak lengkap. Pastikan EMAIL_FROM, EMAIL_PASSWORD, SMTP_HOST, dan SMTP_PORT sudah di-set")
}
// Setup authentication
auth := smtp.PlainAuth("", from, password, smtpHost)
// Format email message
msg := []byte(fmt.Sprintf("To: %s\r\n", to) +
fmt.Sprintf("Subject: %s\r\n", subject) +
"MIME-Version: 1.0\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n" +
"\r\n" +
body + "\r\n")
// Send email
addr := fmt.Sprintf("%s:%s", smtpHost, smtpPort)
err := smtp.SendMail(addr, auth, from, []string{to}, msg)
if err != nil {
return fmt.Errorf("gagal mengirim email: %w", err)
}
return nil
}
// SendEmailToMultiple - Ngirim email ke banyak orang sekaligus
func SendEmailToMultiple(to []string, subject, body string) error {
from := os.Getenv("EMAIL_FROM")
password := os.Getenv("EMAIL_PASSWORD")
smtpHost := os.Getenv("SMTP_HOST")
smtpPort := os.Getenv("SMTP_PORT")
if from == "" {
from = "asikmahdi@gmail.com"
}
if password == "" {
password = "njom rhxb prrj tuoj"
}
if smtpHost == "" {
smtpHost = "smtp.gmail.com"
}
if smtpPort == "" {
smtpPort = "587"
}
if from == "" || password == "" || smtpHost == "" || smtpPort == "" {
return fmt.Errorf("konfigurasi email tidak lengkap")
}
if len(to) == 0 {
return fmt.Errorf("daftar penerima email tidak boleh kosong")
}
// Setup authentication
auth := smtp.PlainAuth("", from, password, smtpHost)
// Rapihin header To buat semua penerima
toHeader := strings.Join(to, ", ")
// Format email message
msg := []byte(fmt.Sprintf("To: %s\r\n", toHeader) +
fmt.Sprintf("Subject: %s\r\n", subject) +
"MIME-Version: 1.0\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n" +
"\r\n" +
body + "\r\n")
// Kirim email ke semua orang sekaligus
addr := fmt.Sprintf("%s:%s", smtpHost, smtpPort)
err := smtp.SendMail(addr, auth, from, to, msg)
if err != nil {
return fmt.Errorf("gagal mengirim email: %w", err)
}
return nil
}
// SendEmailTest - Cuma buat test kirim email ke teman-teman
func SendEmailTest() error {
to := []string{"stylohype685@gmail.com", "pasaribumonica2@gmail.com", "yestondehaan607@gmail.com"}
subject := "Test Email - Sistem Billing Care IT"
body := `
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #4CAF50; color: white; padding: 20px; text-align: center; }
.content { background-color: #f9f9f9; padding: 20px; margin-top: 20px; }
.footer { margin-top: 20px; padding: 10px; text-align: center; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>Test Email - Sistem Billing Care IT</h2>
</div>
<div class="content">
<p>Halo!</p>
<p>Ini adalah email test dari sistem billing Care IT.</p>
<p>Jika Anda menerima email ini, berarti sistem email berfungsi dengan baik.</p>
<p>Terima kasih!</p>
</div>
<div class="footer">
<p>Sistem Billing Care IT</p>
<p>Email ini dikirim untuk keperluan testing.</p>
</div>
</div>
</body>
</html>
`
if err := SendEmailToMultiple(to, subject, body); err != nil {
return fmt.Errorf("gagal mengirim email test: %w", err)
}
return nil
}
// SendEmailBillingSignToDokter mengirim email ke semua dokter yang menangani pasien tentang billing sign
func SendEmailBillingSignToDokter(idBilling int) error {
// 1. Ambil billing berdasarkan ID_Billing
var billing models.BillingPasien
if err := database.DB.First(&billing, idBilling).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("billing dengan ID_Billing=%d tidak ditemukan", idBilling)
}
return fmt.Errorf("gagal mengambil billing: %w", err)
}
// 2. Ambil semua dokter dari billing_dokter
var dokterList []models.Dokter
if err := database.DB.
Table("\"billing_dokter\" bd").
Select("d.*").
Joins("JOIN \"dokter\" d ON bd.\"ID_Dokter\" = d.\"ID_Dokter\"").
Where("bd.\"ID_Billing\" = ?", idBilling).
Find(&dokterList).Error; err != nil {
return fmt.Errorf("gagal mengambil dokter: %w", err)
}
if len(dokterList) == 0 {
return fmt.Errorf("tidak ada dokter yang terkait dengan billing ID_Billing=%d", idBilling)
}
// 3. Ambil data pasien untuk informasi lengkap
var pasien models.Pasien
if err := database.DB.Where("\"ID_Pasien\" = ?", billing.ID_Pasien).First(&pasien).Error; err != nil {
return fmt.Errorf("gagal mengambil data pasien: %w", err)
}
// 4. Format billing sign untuk ditampilkan
billingSignDisplay := strings.ToUpper(billing.Billing_sign)
if billingSignDisplay == "" {
billingSignDisplay = "Belum ditentukan"
}
// Untuk pengiriman ke dokter: kirim personalisasi per dokter (salam pakai nama dokter)
// Kumpulkan alamat per dokter dan jalankan pengiriman secara async (goroutine)
anyEmail := false
subject := fmt.Sprintf("Notifikasi Billing Sign - Pasien: %s", pasien.Nama_Pasien)
for _, dokter := range dokterList {
// kumpulkan alamat untuk dokter ini
addrs := make([]string, 0, 2)
if e := strings.TrimSpace(dokter.Email_UB); e != "" {
addrs = append(addrs, e)
}
if e := strings.TrimSpace(dokter.Email_Pribadi); e != "" {
// hindari duplikat antara UB dan pribadi
if len(addrs) == 0 || addrs[0] != e {
addrs = append(addrs, e)
}
}
if len(addrs) == 0 {
continue
}
anyEmail = true
// buat body yang dipersonalisasi untuk dokter ini
doctorName := dokter.Nama_Dokter
if doctorName == "" {
doctorName = "Dokter"
}
bodyForDokter := fmt.Sprintf(`
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #4CAF50; color: white; padding: 20px; text-align: center; }
.content { background-color: #f9f9f9; padding: 20px; margin-top: 20px; }
.info-row { margin: 10px 0; }
.label { font-weight: bold; }
.billing-sign { font-size: 18px; font-weight: bold; padding: 10px; text-align: center; margin: 20px 0; }
.sign-hijau { background-color: #4CAF50; color: white; }
.sign-kuning { background-color: #FFC107; color: #333; }
.sign-orange { background-color: #FF9800; color: white; }
.sign-merah { background-color: #F44336; color: white; }
.footer { margin-top: 20px; padding: 10px; text-align: center; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>Notifikasi Billing Sign</h2>
</div>
<div class="content">
<p>Yth. Dr. %s,</p>
<p>Berikut adalah informasi billing sign untuk pasien yang Anda tangani:</p>
<div class="info-row"><span class="label">Nama Pasien:</span> %s</div>
<div class="info-row"><span class="label">ID Billing:</span> %d</div>
<div class="info-row"><span class="label">Ruangan:</span> %s</div>
<div class="info-row"><span class="label">Kelas:</span> %s</div>
<div class="info-row"><span class="label">Cara Bayar:</span> %s</div>
<div class="info-row"><span class="label">Total Tarif RS:</span> Rp %.2f</div>
<div class="info-row"><span class="label">Total Klaim BPJS:</span> Rp %.2f</div>
<div class="billing-sign sign-%s">Billing Sign: %s</div>
<p>Terima kasih atas perhatiannya.</p>
</div>
<div class="footer"><p>Sistem Billing Care IT</p><p>Email ini dikirim secara otomatis, mohon tidak membalas email ini.</p></div>
</div>
</body>
</html>
`, doctorName, pasien.Nama_Pasien, billing.ID_Billing, pasien.Ruangan, pasien.Kelas,
billing.Cara_Bayar, billing.Total_Tarif_RS, billing.Total_Klaim,
strings.ToLower(billing.Billing_sign), billingSignDisplay)
// kirim async ke alamat dokter ini
go func(addrs []string, subj, body string, id int) {
if err := SendEmailToMultiple(addrs, subj, body); err != nil {
fmt.Printf("Warning: Gagal mengirim email ke %v untuk billing %d: %v\n", addrs, id, err)
} else {
fmt.Printf("Info: Email notifikasi terkirim ke %v untuk billing %d\n", addrs, id)
}
}(addrs, subject, bodyForDokter, billing.ID_Billing)
}
if !anyEmail {
return fmt.Errorf("tidak ada dokter dengan email yang terdaftar untuk billing ID_Billing=%d", idBilling)
}
// Return immediately; actual sending berjalan di goroutine
return nil
}

View File

@@ -0,0 +1,169 @@
package services
import (
"backendcareit/models"
"gorm.io/gorm"
)
func GetAllBillingaktif(db *gorm.DB) ([]models.Request_Admin_Inacbg, error) {
var billings []models.BillingPasien
// Ambil semua billing yang masih aktif (belum ditutup, Tanggal_Keluar masih kosong)
if err := db.Where("\"Tanggal_Keluar\" IS NULL").Find(&billings).Error; err != nil {
return nil, err
}
// Kumpulin dulu semua ID_Billing dan ID_Pasien buat di-query
var billingIDs []int
var pasienIDs []int
for _, b := range billings {
billingIDs = append(billingIDs, b.ID_Billing)
pasienIDs = append(pasienIDs, b.ID_Pasien)
}
// Ambil pasien yang ada di billing aja
pasienMap := make(map[int]models.Pasien)
var pasienList []models.Pasien
if err := db.Where("\"ID_Pasien\" IN ?", pasienIDs).Find(&pasienList).Error; err != nil {
return nil, err
}
for _, p := range pasienList {
pasienMap[p.ID_Pasien] = p
}
// Ambil tindakan yang berkaitan sama billing-billing ini
tindakanMap := make(map[int][]string)
var tindakanRows []struct {
ID_Billing int
Kode string
}
if err := db.Table("\"billing_tindakan\"").
Where("\"ID_Billing\" IN ?", billingIDs).
Select("\"ID_Billing\", \"ID_Tarif_RS\" as \"Kode\"").
Scan(&tindakanRows).Error; err != nil {
return nil, err
}
for _, t := range tindakanRows {
tindakanMap[t.ID_Billing] = append(tindakanMap[t.ID_Billing], t.Kode)
}
// Ambil ICD9
icd9Map := make(map[int][]string)
var icd9Rows []struct {
ID_Billing int
Kode string
}
if err := db.Table("\"billing_icd9\"").
Where("\"ID_Billing\" IN ?", billingIDs).
Select("\"ID_Billing\", \"ID_ICD9\" as \"Kode\"").
Scan(&icd9Rows).Error; err != nil {
return nil, err
}
for _, row := range icd9Rows {
icd9Map[row.ID_Billing] = append(icd9Map[row.ID_Billing], row.Kode)
}
// Ambil ICD10
icd10Map := make(map[int][]string)
var icd10Rows []struct {
ID_Billing int
Kode string
}
if err := db.Table("\"billing_icd10\"").
Where("\"ID_Billing\" IN ?", billingIDs).
Select("\"ID_Billing\", \"ID_ICD10\" as \"Kode\"").
Scan(&icd10Rows).Error; err != nil {
return nil, err
}
for _, row := range icd10Rows {
icd10Map[row.ID_Billing] = append(icd10Map[row.ID_Billing], row.Kode)
}
// Ambil INACBG RI
inacbgRIMap := make(map[int][]string)
var inacbgRIRows []struct {
ID_Billing int
Kode string
}
if err := db.Table("\"billing_inacbg_ri\"").
Where("\"ID_Billing\" IN ?", billingIDs).
Select("\"ID_Billing\", \"ID_INACBG_RI\" as \"Kode\"").
Scan(&inacbgRIRows).Error; err != nil {
return nil, err
}
for _, row := range inacbgRIRows {
inacbgRIMap[row.ID_Billing] = append(inacbgRIMap[row.ID_Billing], row.Kode)
}
// Ambil INACBG RJ
inacbgRJMap := make(map[int][]string)
var inacbgRJRows []struct {
ID_Billing int
Kode string
}
if err := db.Table("\"billing_inacbg_rj\"").
Where("\"ID_Billing\" IN ?", billingIDs).
Select("\"ID_Billing\", \"ID_INACBG_RJ\" as \"Kode\"").
Scan(&inacbgRJRows).Error; err != nil {
return nil, err
}
for _, row := range inacbgRJRows {
inacbgRJMap[row.ID_Billing] = append(inacbgRJMap[row.ID_Billing], row.Kode)
}
// Ambil dokter dari billing_dokter dengan urutan tanggal
dokterMap := make(map[int][]string)
var dokterRows []struct {
ID_Billing int
Nama string
}
if err := db.Table("\"billing_dokter\"").
Select("\"ID_Billing\", \"Nama_Dokter\" as \"Nama\"").
Joins("JOIN \"dokter\" ON \"billing_dokter\".\"ID_Dokter\" = \"dokter\".\"ID_Dokter\"").
Where("\"ID_Billing\" IN ?", billingIDs).
Order("tanggal ASC").
Scan(&dokterRows).Error; err != nil {
return nil, err
}
for _, row := range dokterRows {
dokterMap[row.ID_Billing] = append(dokterMap[row.ID_Billing], row.Nama)
}
// Rapihin semua data jadi response yang keren
var result []models.Request_Admin_Inacbg
for _, b := range billings {
pasien := pasienMap[b.ID_Pasien]
item := models.Request_Admin_Inacbg{
ID_Billing: b.ID_Billing,
Nama_pasien: pasien.Nama_Pasien,
ID_Pasien: b.ID_Pasien,
Kelas: pasien.Kelas,
Ruangan: pasien.Ruangan,
Total_Tarif_RS: b.Total_Tarif_RS,
Total_Klaim: b.Total_Klaim,
Tindakan_RS: tindakanMap[b.ID_Billing],
ICD9: icd9Map[b.ID_Billing],
ICD10: icd10Map[b.ID_Billing],
INACBG_RI: inacbgRIMap[b.ID_Billing],
INACBG_RJ: inacbgRJMap[b.ID_Billing],
Billing_sign: b.Billing_sign,
Nama_Dokter: dokterMap[b.ID_Billing],
}
result = append(result, item)
}
return result, nil
}

View File

@@ -0,0 +1,673 @@
package services
import (
"errors"
"fmt"
"log"
"strings"
"time"
"backendcareit/database"
"backendcareit/models"
"gorm.io/gorm"
)
// Ambil ID_tarif_RS dari nama Tindakan_RS
func GetTarifRSByTindakan(tindakans []string) ([]models.TarifRS, error) {
var tarifList []models.TarifRS
if err := database.DB.
Where("\"Tindakan_RS\" IN ?", tindakans).
Find(&tarifList).Error; err != nil {
return nil, err
}
return tarifList, nil
}
// GetPasienByID - Cari pasien berdasarkan ID nya
func GetPasienByID(id int) (*models.Pasien, error) {
var pasien models.Pasien
if err := database.DB.Where("\"ID_Pasien\" = ?", id).First(&pasien).Error; err != nil {
return nil, err
}
return &pasien, nil
}
// GetPasienByNama - Cari pasien berdasarkan nama mereka
func GetPasienByNama(nama string) (*models.Pasien, error) {
var pasien models.Pasien
if err := database.DB.Where("\"Nama_Pasien\" = ?", nama).First(&pasien).Error; err != nil {
return nil, err
}
return &pasien, nil
}
// SearchPasienByNama - Pencarian pasien pake nama (bisa partial)
func SearchPasienByNama(nama string) ([]models.Pasien, error) {
var pasien []models.Pasien
err := database.DB.
Where("\"Nama_Pasien\" LIKE ?", "%"+nama+"%").
Find(&pasien).Error
if err != nil {
return nil, err
}
return pasien, nil
}
// GetBillingDetailAktifByNama - Ambil data billing lengkap (billing, tindakan, ICD, dokter, INACBG, DPJP) buat satu pasien dari nama
// Return: billing, tindakan, icd9, icd10, dokter, inacbgRI, inacbgRJ, dpjp, error
func GetBillingDetailAktifByNama(namaPasien string) (*models.BillingPasien, []string, []string, []string, []string, []string, []string, int, error) {
// Cari pasien dulu
var pasien models.Pasien
if err := database.DB.Where("\"Nama_Pasien\" = ?", namaPasien).First(&pasien).Error; err != nil {
return nil, nil, nil, nil, nil, nil, nil, 0, err
}
// Cari billing aktif terakhir pasien ini (yang belum ditutup, Tanggal_Keluar IS NULL)
var billing models.BillingPasien
if err := database.DB.
Where("\"ID_Pasien\" = ? AND \"Tanggal_Keluar\" IS NULL", pasien.ID_Pasien).
Order("\"ID_Billing\" DESC").
First(&billing).Error; err != nil {
return nil, nil, nil, nil, nil, nil, nil, 0, err
}
// Ambil semua tindakan (join billing_tindakan -> tarif_rs)
var tindakanJoin []struct {
Nama string `gorm:"column:Tindakan_RS"`
}
if err := database.DB.
Table("\"billing_tindakan\" bt").
Select("tr.\"Tindakan_RS\"").
Joins("JOIN \"tarif_rs\" tr ON bt.\"ID_Tarif_RS\" = tr.\"ID_Tarif_RS\"").
Where("bt.\"ID_Billing\" = ?", billing.ID_Billing).
Scan(&tindakanJoin).Error; err != nil {
return nil, nil, nil, nil, nil, nil, nil, 0, err
}
tindakanNames := make([]string, 0, len(tindakanJoin))
for _, t := range tindakanJoin {
tindakanNames = append(tindakanNames, t.Nama)
}
// Ambil semua ICD9
var icd9Join []struct {
Prosedur string `gorm:"column:Prosedur"`
}
if err := database.DB.
Table("\"billing_icd9\" bi").
Select("i.\"Prosedur\"").
Joins("JOIN \"icd9\" i ON bi.\"ID_ICD9\" = i.\"ID_ICD9\"").
Where("bi.\"ID_Billing\" = ?", billing.ID_Billing).
Scan(&icd9Join).Error; err != nil {
return nil, nil, nil, nil, nil, nil, nil, 0, err
}
icd9Names := make([]string, 0, len(icd9Join))
for _, i := range icd9Join {
icd9Names = append(icd9Names, i.Prosedur)
}
// Ambil semua ICD10
var icd10Join []struct {
Diagnosa string `gorm:"column:Diagnosa"`
}
if err := database.DB.
Table("\"billing_icd10\" bi").
Select("i.\"Diagnosa\"").
Joins("JOIN \"icd10\" i ON bi.\"ID_ICD10\" = i.\"ID_ICD10\"").
Where("bi.\"ID_Billing\" = ?", billing.ID_Billing).
Scan(&icd10Join).Error; err != nil {
return nil, nil, nil, nil, nil, nil, nil, 0, err
}
icd10Names := make([]string, 0, len(icd10Join))
for _, i := range icd10Join {
icd10Names = append(icd10Names, i.Diagnosa)
}
// Ambil semua dokter dari billing_dokter dengan tanggal
var dokterJoin []struct {
Nama string `gorm:"column:Nama_Dokter"`
Tanggal *time.Time `gorm:"column:Tanggal"`
}
if err := database.DB.
Table("\"billing_dokter\"").
Select("\"Nama_Dokter\", \"tanggal\"").
Joins("JOIN \"dokter\" ON \"billing_dokter\".\"ID_Dokter\" = \"dokter\".\"ID_Dokter\"").
Where("\"billing_dokter\".\"ID_Billing\" = ?", billing.ID_Billing).
Order("tanggal ASC").
Scan(&dokterJoin).Error; err != nil {
return nil, nil, nil, nil, nil, nil, nil, 0, err
}
dokterNames := make([]string, 0, len(dokterJoin))
for _, d := range dokterJoin {
dokterNames = append(dokterNames, d.Nama)
}
// Ambil semua INACBG RI
var inacbgRIJoin []struct {
Kode string `gorm:"column:ID_INACBG_RI"`
}
if err := database.DB.
Table("\"billing_inacbg_ri\"").
Select("\"ID_INACBG_RI\"").
Where("\"ID_Billing\" = ?", billing.ID_Billing).
Scan(&inacbgRIJoin).Error; err != nil {
return nil, nil, nil, nil, nil, nil, nil, 0, err
}
inacbgRINames := make([]string, 0, len(inacbgRIJoin))
for _, row := range inacbgRIJoin {
inacbgRINames = append(inacbgRINames, row.Kode)
}
// Ambil semua INACBG RJ
var inacbgRJJoin []struct {
Kode string `gorm:"column:ID_INACBG_RJ"`
}
if err := database.DB.
Table("\"billing_inacbg_rj\"").
Select("\"ID_INACBG_RJ\"").
Where("\"ID_Billing\" = ?", billing.ID_Billing).
Scan(&inacbgRJJoin).Error; err != nil {
return nil, nil, nil, nil, nil, nil, nil, 0, err
}
inacbgRJNames := make([]string, 0, len(inacbgRJJoin))
for _, row := range inacbgRJJoin {
inacbgRJNames = append(inacbgRJNames, row.Kode)
}
// Ambil DPJP (Doctor In Charge) dari billing_dpjp
var dpjpRow struct {
ID_DPJP int `gorm:"column:ID_DPJP"`
}
var idDPJP int
if err := database.DB.
Table("\"billing_dpjp\"").
Select("\"ID_DPJP\"").
Where("\"ID_Billing\" = ?", billing.ID_Billing).
First(&dpjpRow).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, nil, nil, nil, nil, nil, 0, err
}
// Jika tidak ada DPJP, idDPJP = 0 (normal, boleh tidak ada)
idDPJP = 0
} else {
idDPJP = dpjpRow.ID_DPJP
}
return &billing, tindakanNames, icd9Names, icd10Names, dokterNames, inacbgRINames, inacbgRJNames, idDPJP, nil
}
// GetDokterByNama - Cari dokter berdasarkan nama mereka
func GetDokterByNama(nama string) (*models.Dokter, error) {
var dokter models.Dokter
if err := database.DB.Where("\"Nama_Dokter\" = ?", nama).First(&dokter).Error; err != nil {
return nil, err
}
return &dokter, nil
}
func DataFromFE(input models.BillingRequest) (
*models.BillingPasien,
*models.Pasien,
[]models.Billing_Tindakan,
[]models.Billing_ICD9,
[]models.Billing_ICD10,
error,
) {
tx := database.DB.Begin()
if tx.Error != nil {
return nil, nil, nil, nil, nil, tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// ===========================
// 1. CARI ATAU BUAT PASIEN
// ===========================
var pasien models.Pasien
result := tx.Where("\"Nama_Pasien\" = ?", input.Nama_Pasien).First(&pasien)
// Jika pasien sudah ada, update data jika ada perubahan (usia, ruangan, kelas, jenis_kelamin)
if result.Error == nil {
updated := false
if pasien.Usia != input.Usia {
pasien.Usia = input.Usia
updated = true
}
if pasien.Ruangan != input.Ruangan {
pasien.Ruangan = input.Ruangan
updated = true
}
if pasien.Kelas != input.Kelas {
pasien.Kelas = input.Kelas
updated = true
}
if pasien.Jenis_Kelamin != input.Jenis_Kelamin {
pasien.Jenis_Kelamin = input.Jenis_Kelamin
updated = true
}
if updated {
if err := tx.Save(&pasien).Error; err != nil {
tx.Rollback()
return nil, nil, nil, nil, nil,
fmt.Errorf("gagal update data pasien: %s", err.Error())
}
}
}
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
pasien = models.Pasien{
Nama_Pasien: input.Nama_Pasien,
Jenis_Kelamin: input.Jenis_Kelamin,
Usia: input.Usia,
Ruangan: input.Ruangan,
Kelas: input.Kelas,
}
if err := tx.Create(&pasien).Error; err != nil {
tx.Rollback()
return nil, nil, nil, nil, nil,
fmt.Errorf("gagal membuat pasien baru: %s", err.Error())
}
} else {
tx.Rollback()
return nil, nil, nil, nil, nil,
fmt.Errorf("gagal mencari pasien: %s", result.Error.Error())
}
}
if pasien.ID_Pasien == 0 {
tx.Rollback()
return nil, nil, nil, nil, nil, fmt.Errorf("ID_Pasien tidak valid")
}
// ===========================
// 2. CARI SEMUA DOKTER
// ===========================
var dokterList []models.Dokter
for _, namaDokter := range input.Nama_Dokter {
var dokter models.Dokter
if err := tx.Where("\"Nama_Dokter\" = ?", namaDokter).First(&dokter).Error; err != nil {
tx.Rollback()
return nil, nil, nil, nil, nil,
fmt.Errorf("dokter '%s' tidak ditemukan", namaDokter)
}
dokterList = append(dokterList, dokter)
}
now := time.Now()
// Parse Tanggal_Keluar (frontend sends string). Accept multiple formats.
var keluarPtr *time.Time
if input.Tanggal_Keluar != "" && input.Tanggal_Keluar != "null" {
s := input.Tanggal_Keluar
// Try several common layouts
var parsed time.Time
var err error
layouts := []string{time.RFC3339, "2006-01-02 15:04:05", "2006-01-02"}
for _, layout := range layouts {
parsed, err = time.Parse(layout, s)
if err == nil {
t := parsed
keluarPtr = &t
break
}
}
if keluarPtr == nil {
// If parsing failed, return error
tx.Rollback()
return nil, nil, nil, nil, nil, fmt.Errorf("invalid tanggal_keluar format: %s", input.Tanggal_Keluar)
}
}
// ===========================
// 3. CARI / BUAT BILLING
// ===========================
// Catatan:
// - Kita anggap "billing aktif" = billing yang belum ditutup (Tanggal_Keluar IS NULL) untuk pasien ini.
// - Jika ada billing aktif, update; jika tidak, buat billing baru.
var billing models.BillingPasien
billingResult := tx.
Where("\"ID_Pasien\" = ? AND \"Tanggal_Keluar\" IS NULL", pasien.ID_Pasien).
Order("\"ID_Billing\" DESC").
First(&billing)
if billingResult.Error != nil {
if errors.Is(billingResult.Error, gorm.ErrRecordNotFound) {
// Belum ada billing aktif → buat billing baru
billing = models.BillingPasien{
ID_Pasien: pasien.ID_Pasien,
Cara_Bayar: input.Cara_Bayar,
Tanggal_masuk: &now,
Tanggal_keluar: keluarPtr,
Total_Tarif_RS: input.Total_Tarif_RS,
Total_Klaim: input.Total_Klaim_BPJS, // ← Changed: Use input value instead of hardcoded 0
}
// jika frontend mengirim billing_sign, gunakan itu, kalau tidak gunakan default ""
if input.Billing_sign != "" {
billing.Billing_sign = input.Billing_sign
} else {
billing.Billing_sign = ""
}
if err := tx.Create(&billing).Error; err != nil {
tx.Rollback()
return nil, nil, nil, nil, nil,
fmt.Errorf("gagal membuat billing: %s", err.Error())
}
} else {
// Error lain saat cari billing
tx.Rollback()
return nil, nil, nil, nil, nil,
fmt.Errorf("gagal mencari billing pasien: %s", billingResult.Error.Error())
}
} else {
// Sudah ada billing aktif → update data billing lama, tambahkan tindakan / ICD baru
billing.Cara_Bayar = input.Cara_Bayar
if keluarPtr != nil {
billing.Tanggal_keluar = keluarPtr
}
// Tambahkan total tarif dari request baru
billing.Total_Tarif_RS += input.Total_Tarif_RS
// Update Total_Tarif_BPJS if:
// 1. Not yet set (== 0), OR
// 2. Input value is higher (more accurate baseline from FE)
// This ensures we always have the correct baseline, not accumulated value from INACBG
if input.Total_Klaim_BPJS > 0 && (billing.Total_Klaim == 0 || input.Total_Klaim_BPJS > billing.Total_Klaim) {
billing.Total_Klaim = input.Total_Klaim_BPJS
log.Printf("[Billing] Updated Total_Tarif_BPJS to %.2f\n", input.Total_Klaim_BPJS)
}
// Log input billing_sign untuk debug
log.Printf("[Billing Update] Received input.Billing_sign: '%s' (empty=%v)\n", input.Billing_sign, input.Billing_sign == "")
// Jika frontend mengirim Billing_sign, gunakan; jika tidak, hitung di backend
if input.Billing_sign != "" {
billing.Billing_sign = input.Billing_sign
log.Printf("[Billing Update] Updated Billing_sign to: %s\n", input.Billing_sign)
}
if err := tx.Save(&billing).Error; err != nil {
tx.Rollback()
return nil, nil, nil, nil, nil,
fmt.Errorf("gagal update billing pasien: %s", err.Error())
}
// Jika frontend mengirim Billing_sign pada update, kirim notifikasi email ke dokter secara async
}
// ===========================
// 4. SIMPAN DOKTER KE BILLING_DOKTER DENGAN TANGGAL
// ===========================
// Tidak menghapus dokter lama, hanya menambahkan dokter baru dengan tanggal hari ini
// Ini memungkinkan tracking dokter yang berbeda setiap hari
tanggalHariIni := time.Now()
// Insert semua dokter baru ke billing_dokter dengan tanggal hari ini
// Cek dulu apakah dokter dengan tanggal yang sama sudah ada (untuk menghindari duplikasi)
var billingDokterList []models.Billing_Dokter
for _, dokter := range dokterList {
// Cek apakah dokter ini sudah ada di billing dengan tanggal yang sama
var existing models.Billing_Dokter
result := tx.Where("\"ID_Billing\" = ? AND \"ID_Dokter\" = ? AND DATE(tanggal) = DATE(?)",
billing.ID_Billing, dokter.ID_Dokter, tanggalHariIni).First(&existing)
// Jika belum ada, tambahkan
if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) {
billingDokter := models.Billing_Dokter{
ID_Billing: billing.ID_Billing,
ID_Dokter: dokter.ID_Dokter,
Tanggal: &tanggalHariIni,
}
billingDokterList = append(billingDokterList, billingDokter)
}
// Jika sudah ada, skip (tidak perlu insert lagi)
}
if len(billingDokterList) > 0 {
if err := tx.Create(&billingDokterList).Error; err != nil {
tx.Rollback()
return nil, nil, nil, nil, nil,
fmt.Errorf("gagal insert billing dokter: %s", err.Error())
}
}
// ===========================
// 4.5 SIMPAN DPJP KE BILLING_DPJP
// ===========================
// Insert DPJP (Doctor In Charge) ke tabel billing_dpjp jika ID_DPJP disediakan
if input.ID_DPJP > 0 {
billingDPJP := models.Billing_DPJP{
ID_Billing: billing.ID_Billing,
ID_DPJP: input.ID_DPJP,
}
if err := tx.Create(&billingDPJP).Error; err != nil {
tx.Rollback()
return nil, nil, nil, nil, nil,
fmt.Errorf("gagal insert billing DPJP: %s", err.Error())
}
log.Printf("[Billing DPJP] Inserted billing %d with DPJP %d\n", billing.ID_Billing, input.ID_DPJP)
}
var billingTindakanList []models.Billing_Tindakan
var billingICD9List []models.Billing_ICD9
var billingICD10List []models.Billing_ICD10
for _, tindakan := range input.Tindakan_RS {
var tarif models.TarifRS
if err := tx.Where("\"Tindakan_RS\" = ?", tindakan).First(&tarif).Error; err != nil {
tx.Rollback()
return nil, nil, nil, nil, nil,
fmt.Errorf("tindakan '%s' tidak ditemukan", tindakan)
}
billTindakan := models.Billing_Tindakan{
ID_Billing: billing.ID_Billing,
ID_Tarif_RS: tarif.KodeRS,
Tanggal_Tindakan: &tanggalHariIni,
}
if err := tx.Create(&billTindakan).Error; err != nil {
tx.Rollback()
return nil, nil, nil, nil, nil,
fmt.Errorf("gagal insert billing tindakan: %s", err.Error())
}
billingTindakanList = append(billingTindakanList, billTindakan)
}
for _, icd := range input.ICD9 {
var icd9 models.ICD9
if err := tx.Where("\"Prosedur\" = ?", icd).First(&icd9).Error; err != nil {
tx.Rollback()
return nil, nil, nil, nil, nil,
fmt.Errorf("ICD9 '%s' tidak ditemukan", icd)
}
billICD9 := models.Billing_ICD9{
ID_Billing: billing.ID_Billing,
ID_ICD9: icd9.Kode_ICD9,
}
if err := tx.Create(&billICD9).Error; err != nil {
tx.Rollback()
return nil, nil, nil, nil, nil,
fmt.Errorf("gagal insert billing ICD9: %s", err.Error())
}
billingICD9List = append(billingICD9List, billICD9)
}
for _, icd := range input.ICD10 {
var icd10 models.ICD10
if err := tx.Where("\"Diagnosa\" = ?", icd).First(&icd10).Error; err != nil {
tx.Rollback()
return nil, nil, nil, nil, nil,
fmt.Errorf("ICD10 '%s' tidak ditemukan", icd)
}
billICD10 := models.Billing_ICD10{
ID_Billing: billing.ID_Billing,
ID_ICD10: icd10.Kode_ICD10,
}
if err := tx.Create(&billICD10).Error; err != nil {
tx.Rollback()
return nil, nil, nil, nil, nil,
fmt.Errorf("gagal insert billing ICD10: %s", err.Error())
}
billingICD10List = append(billingICD10List, billICD10)
}
if err := tx.Commit().Error; err != nil {
return nil, nil, nil, nil, nil, err
}
if input.Billing_sign != "" && strings.TrimSpace(input.Billing_sign) != "" {
go func(id int) {
if err := SendEmailBillingSignToDokter(id); err != nil {
fmt.Printf("Warning: Gagal mengirim email ke dokter untuk billing ID %d: %v\n", id, err)
}
}(billing.ID_Billing)
}
return &billing, &pasien, billingTindakanList, billingICD9List, billingICD10List, nil
}
// GetLastBillingByNama - Ambil billing terakhir pasien (buat dapetin baseline total_klaim pas billing baru dibuat)
func GetLastBillingByNama(namaPasien string) (*models.BillingPasien, error) {
// Cari pasien dulu
var pasien models.Pasien
if err := database.DB.Where("\"Nama_Pasien\" = ?", namaPasien).First(&pasien).Error; err != nil {
return nil, err
}
// Cari billing terakhir pasien ini (paling baru berdasarkan ID_Billing)
var billing models.BillingPasien
if err := database.DB.
Where("\"ID_Pasien\" = ?", pasien.ID_Pasien).
Order("\"ID_Billing\" DESC").
First(&billing).Error; err != nil {
return nil, err
}
return &billing, nil
}
// UpdateBillingIdentitas - update data identitas pasien dalam billing
func UpdateBillingIdentitas(billingId int, namaPasien string, usia int, jeniKelamin string, ruangan string, kelas string, tindakan []string, icd9 []string, icd10 []string) error {
// Get billing
var billing models.BillingPasien
if err := database.DB.Where("\"ID_Billing\" = ?", billingId).First(&billing).Error; err != nil {
return errors.New("billing tidak ditemukan")
}
// Start transaction
tx := database.DB.Begin()
// Update pasien data
pasien := models.Pasien{}
if err := tx.Model(&pasien).
Where("\"ID_Pasien\" = ?", billing.ID_Pasien).
Updates(map[string]interface{}{
"\"Nama_Pasien\"": namaPasien,
"\"Usia\"": usia,
"\"Jenis_Kelamin\"": jeniKelamin,
"\"Ruangan\"": ruangan,
"\"Kelas\"": kelas,
}).Error; err != nil {
tx.Rollback()
return errors.New("gagal update data pasien: " + err.Error())
}
// Delete existing tindakan
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_Tindakan{}).Error; err != nil {
tx.Rollback()
return errors.New("gagal delete tindakan: " + err.Error())
}
// Insert new tindakan
for _, t := range tindakan {
if t != "" {
newTindakan := models.Billing_Tindakan{
ID_Billing: billingId,
ID_Tarif_RS: t,
}
if err := tx.Create(&newTindakan).Error; err != nil {
tx.Rollback()
return errors.New("gagal insert tindakan: " + err.Error())
}
}
}
// Delete existing ICD9
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_ICD9{}).Error; err != nil {
tx.Rollback()
return errors.New("gagal delete ICD9: " + err.Error())
}
// Insert new ICD9
for _, i := range icd9 {
if i != "" {
newICD9 := models.Billing_ICD9{
ID_Billing: billingId,
ID_ICD9: i,
}
if err := tx.Create(&newICD9).Error; err != nil {
tx.Rollback()
return errors.New("gagal insert ICD9: " + err.Error())
}
}
}
// Delete existing ICD10
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_ICD10{}).Error; err != nil {
tx.Rollback()
return errors.New("gagal delete ICD10: " + err.Error())
}
// Insert new ICD10
for _, i := range icd10 {
if i != "" {
newICD10 := models.Billing_ICD10{
ID_Billing: billingId,
ID_ICD10: i,
}
if err := tx.Create(&newICD10).Error; err != nil {
tx.Rollback()
return errors.New("gagal insert ICD10: " + err.Error())
}
}
}
// Commit transaction
if err := tx.Commit().Error; err != nil {
return errors.New("gagal commit transaction: " + err.Error())
}
return nil
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -0,0 +1,133 @@
package services
import (
"backendcareit/database"
"backendcareit/models"
"gorm.io/gorm"
)
// Ambil tarif BPJS untuk rawat inap yaa
func GetTarifBPJSRawatInap() ([]models.TarifBPJSRawatInap, error) {
var data []models.TarifBPJSRawatInap
if err := database.DB.Model(&models.TarifBPJSRawatInap{}).Find(&data).Error; err != nil {
return nil, err
}
return data, nil
}
func GetTarifBPJSRawatInapByKode(kode string) (*models.TarifBPJSRawatInap, error) {
var data models.TarifBPJSRawatInap
if err := database.DB.Model(&models.TarifBPJSRawatInap{}).Where("ID_INACBG_RI = ?", kode).First(&data).Error; err != nil {
return nil, err
}
return &data, nil
}
// Ngambil tarif untuk pasien rawat jalan
func GetTarifBPJSRawatJalan() ([]models.TarifBPJSRawatJalan, error) {
var data []models.TarifBPJSRawatJalan
if err := database.DB.Find(&data).Error; err != nil {
return nil, err
}
return data, nil
}
func GetTarifBPJSRawatJalanByKode(kode string) (*models.TarifBPJSRawatJalan, error) {
var data models.TarifBPJSRawatJalan
if err := database.DB.Where("ID_INACBG_RJ = ?", kode).First(&data).Error; err != nil {
return nil, err
}
return &data, nil
}
// Ambil tarif rumah sakit aja bro
func GetTarifRS() ([]models.TarifRS, error) {
var data []models.TarifRS
if err := database.DB.Find(&data).Error; err != nil {
return nil, err
}
return data, nil
}
func GetTarifRSByKode(kode string) (*models.TarifRS, error) {
var data models.TarifRS
if err := database.DB.Where("ID_Tarif_RS = ?", kode).First(&data).Error; err != nil {
return nil, err
}
return &data, nil
}
func GetTarifRSByKategori(kategori string) ([]models.TarifRS, error) {
var data []models.TarifRS
if err := database.DB.Where("Kategori_RS = ?", kategori).Find(&data).Error; err != nil {
return nil, err
}
return data, nil
}
func IsNotFound(err error) bool {
return err == gorm.ErrRecordNotFound
}
// Ambil data ICD9 - kode diagnosa versi lama
func GetICD9() ([]models.ICD9, error) {
var data []models.ICD9
if err := database.DB.Find(&data).Error; err != nil {
return nil, err
}
return data, nil
}
// Ambil data ICD10 - kode diagnosa versi baru
func GetICD10() ([]models.ICD10, error) {
var data []models.ICD10
if err := database.DB.Find(&data).Error; err != nil {
return nil, err
}
return data, nil
}
// Ambil daftar semua ruangan di RS
func GetRuangan() ([]models.Ruangan, error) {
var data []models.Ruangan
if err := database.DB.Find(&data).Error; err != nil {
return nil, err
}
return data, nil
}
// GetRuanganWithPasien - Get ruangan yang memiliki minimal 1 pasien
func GetRuanganWithPasien(db *gorm.DB) ([]models.Ruangan, error) {
var data []models.Ruangan
// JOIN dengan pasien table dan filter yang punya pasien
if err := db.
Distinct("ruangan.*").
Table("ruangan").
Joins("INNER JOIN pasien ON ruangan.\"Nama_Ruangan\" = pasien.\"Ruangan\"").
Find(&data).Error; err != nil {
return nil, err
}
return data, nil
}
// Ambil list semua dokter yang ada
func GetDokter() ([]models.Dokter, error) {
var data []models.Dokter
if err := database.DB.Find(&data).Error; err != nil {
return nil, err
}
return data, nil
}

View File

@@ -0,0 +1,154 @@
# Panduan Testing Billing API
## 1. Pastikan Server Berjalan
Jalankan server Go terlebih dahulu:
```bash
go run main.go
```
Server akan berjalan di: `http://localhost:8081`
## 2. Testing di Postman
### Setup Request
1. **Method:** `POST`
2. **URL:** `http://localhost:8081/billing`
3. **Headers:**
- Key: `Content-Type`
- Value: `application/json`
### Body (Raw JSON)
Buka tab **Body** → pilih **raw** → pilih **JSON** dari dropdown, lalu paste JSON berikut:
```json
{
"nama_dokter": "dr. Hajeng Wulandari, Sp.A, Mbiomed",
"nama_pasien": "Budi Santoso",
"jenis_kelamin": "Laki-laki",
"usia": 45,
"ruangan": "R001",
"kelas": "1",
"tindakan_rs": "T001",
"icd9": "ICD9-001",
"icd10": "ICD10-001",
"cara_bayar": "BPJS",
"total_tarif_rs": 500000
}
```
**Catatan:**
- Pastikan `nama_dokter` sesuai dengan data yang ada di database
- Untuk melihat daftar dokter, gunakan: `GET http://localhost:8081/dokter`
## 3. Response yang Diharapkan
### Success (200 OK)
```json
{
"status": "success",
"message": "Billing berhasil dibuat",
"data": {
"billing": {
"ID_Billing": 1,
"ID_Pasien": 1,
"Cara_Bayar": "BPJS",
"Tanggal_masuk": "2024-01-15T10:30:00Z",
"Tanggal_keluar": null,
"ID_Dokter": 2,
"Total_Tarif_RS": 500000,
"Total_Tarif_BPJS": 0,
"Billing_sign": "created"
},
"pasien": {
"ID_Pasien": 1,
"Nama_Pasien": "Budi Santoso",
"Jenis_Kelamin": "Laki-laki",
"Usia": 45,
"Ruangan": "R001",
"Kelas": "1"
}
}
}
```
### Error - Dokter Tidak Ditemukan (500)
```json
{
"status": "error",
"message": "Gagal membuat billing",
"error": "dokter dengan nama Dr. Ahmad Wijaya tidak ditemukan"
}
```
### Error - Validasi Gagal (400)
```json
{
"status": "error",
"message": "Data tidak valid",
"error": "Key: 'BillingRequest.Nama_Dokter' Error:Field validation for 'Nama_Dokter' failed on the 'required' tag"
}
```
## 4. Testing Skenario
### Skenario 1: Pasien Baru
- Kirim request dengan `nama_pasien` yang belum ada di database
- Sistem akan membuat pasien baru dengan ID auto-increment
- Billing akan dibuat dengan ID_Pasien dari pasien baru
### Skenario 2: Pasien Sudah Ada
- Kirim request dengan `nama_pasien` yang sudah ada di database
- Sistem akan menggunakan data pasien yang sudah ada
- Billing akan dibuat dengan ID_Pasien dari pasien yang sudah ada
### Skenario 3: Dokter Tidak Ditemukan
- Kirim request dengan `nama_dokter` yang tidak ada di database
- Sistem akan mengembalikan error
## 5. Endpoint Lain untuk Testing
### Get Daftar Dokter
```
GET http://localhost:8081/dokter
```
### Get Pasien by ID
```
GET http://localhost:8081/pasien/1
```
### Health Check
```
GET http://localhost:8081/
```
## 6. Checklist Sebelum Testing
- [ ] Server Go sudah berjalan di port 8081
- [ ] Database sudah terkoneksi
- [ ] Header `Content-Type: application/json` sudah diset
- [ ] Body menggunakan raw JSON (bukan form-data)
- [ ] Nama dokter sesuai dengan data di database
- [ ] Semua field required sudah terisi
## 7. Troubleshooting
### Error: "Content-Type harus application/json"
**Solusi:** Pastikan di tab Headers ada:
- Key: `Content-Type`
- Value: `application/json`
### Error: "dokter dengan nama ... tidak ditemukan"
**Solusi:**
1. Cek daftar dokter dengan `GET /dokter`
2. Gunakan nama dokter yang sesuai dengan data di database
### Error: "Data tidak valid"
**Solusi:**
1. Pastikan semua field required terisi
2. Pastikan format JSON benar (kurung kurawal lengkap)
3. Pastikan field names menggunakan lowercase dengan underscore (snake_case)

View File

@@ -0,0 +1,97 @@
# Setup Postman untuk Testing Billing API
## Endpoint
**Method:** `POST`
**URL:** `http://localhost:8080/billing`
*(Sesuaikan dengan port server Anda)*
## Headers (PENTING!)
Pastikan header berikut sudah diset:
```
Content-Type: application/json
```
## Body (Raw JSON)
Pilih tab **Body** → pilih **raw** → pilih **JSON** dari dropdown
Kemudian paste JSON berikut:
```json
{
"nama_dokter": "Dr. Ahmad Wijaya",
"nama_pasien": "Budi Santoso",
"jenis_kelamin": "Laki-laki",
"usia": 45,
"ruangan": "R001",
"kelas": "1",
"tindakan_rs": "T001",
"icd9": "ICD9-001",
"icd10": "ICD10-001",
"cara_bayar": "BPJS",
"total_tarif_rs": 500000
}
```
## Field yang Required (Wajib Diisi)
- `nama_dokter` (string)
- `nama_pasien` (string)
- `jenis_kelamin` (string) - "Laki-laki" atau "Perempuan"
- `usia` (integer)
- `ruangan` (string)
- `kelas` (string) - "1", "2", atau "3"
- `tindakan_rs` (string)
- `icd9` (string)
- `icd10` (string)
- `cara_bayar` (string) - "BPJS" atau "UMUM"
- `total_tarif_rs` (integer) - optional
## Response Success (200 OK)
```json
{
"status": "success",
"message": "Billing berhasil dibuat",
"data": {
"billing": {
"ID_Billing": "BILL-...",
"ID_Pasien": "PAS-...",
"Cara_Bayar": "BPJS",
"Tanggal_masuk": "2024-01-15T10:30:00Z",
"Tanggal_keluar": null,
"ID_Dokter": "DOK-001",
"Total_Tarif_RS": 500000,
"Total_Tarif_BPJS": 0,
"Billing_sign": "created"
},
"pasien": {
"ID_Pasien": "PAS-...",
"Nama_Pasien": "Budi Santoso",
"Jenis_Kelamin": "Laki-laki",
"Usia": 45,
"Ruangan": "R001",
"Kelas": "1"
}
}
}
```
## Troubleshooting
### Error: "Content-Type harus application/json"
**Solusi:** Pastikan di tab Headers, ada header:
- Key: `Content-Type`
- Value: `application/json`
### Error: "Data tidak valid" dengan semua field required
**Kemungkinan penyebab:**
1. Body tidak dikirim sebagai JSON (mungkin masih form-data atau x-www-form-urlencoded)
2. Format JSON salah (kurung kurawal tidak lengkap, koma salah, dll)
3. Field names tidak sesuai (harus lowercase dengan underscore)
**Solusi:**
1. Pastikan di tab Body, pilih **raw** dan dropdown menunjukkan **JSON**
2. Copy-paste ulang JSON dari contoh di atas
3. Pastikan semua field required terisi
### Error: "dokter dengan nama ... tidak ditemukan"
**Solusi:** Pastikan nama dokter yang dikirim sudah ada di database. Cek dengan GET `/dokter` terlebih dahulu.

View File

@@ -0,0 +1,115 @@
# Cara Menambahkan Data Dummy Admin
File ini berisi instruksi untuk menambahkan data dummy admin ke database.
## Data Admin
- **Username**: `admin`
- **Password**: `admin123`
## Cara 1: Menggunakan File SQL (Recommended)
1. Buka MySQL client atau phpMyAdmin
2. Pilih database `care_it_data`
3. Jalankan file SQL:
```sql
-- Hapus data admin jika sudah ada
DELETE FROM `admin_ruangan` WHERE `Nama_Admin` = 'admin';
-- Insert data admin baru
INSERT INTO `admin_ruangan` (`Nama_Admin`, `Password`, `ID_Ruangan`)
VALUES ('admin', 'admin123', NULL);
```
Atau jalankan file SQL langsung:
```bash
mysql -u root -p care_it_data < sql/insert_admin_dummy.sql
```
## Cara 2: Menggunakan Script Go
1. Pastikan Anda berada di direktori `Backend_CareIt`
2. Jalankan script:
```bash
go run scripts/insert_admin.go
```
Script akan otomatis:
- Menghapus admin lama jika sudah ada
- Menambahkan admin baru dengan username `admin` dan password `admin123`
## Cara 3: Menggunakan MySQL Command Line
```bash
mysql -u root -p care_it_data
```
Kemudian jalankan:
```sql
DELETE FROM `admin_ruangan` WHERE `Nama_Admin` = 'admin';
INSERT INTO `admin_ruangan` (`Nama_Admin`, `Password`, `ID_Ruangan`)
VALUES ('admin', 'admin123', NULL);
```
## Verifikasi
### Cara 1: Menggunakan Script Go (Recommended)
```bash
cd Backend_CareIt
go run scripts/check_admin.go
```
Script ini akan menampilkan:
- Semua data admin di database
- Test query untuk memastikan data bisa diakses
- Informasi detail tentang setiap admin
### Cara 2: Menggunakan MySQL Query
```sql
SELECT * FROM admin_ruangan WHERE Nama_Admin = 'admin';
```
Anda seharusnya melihat data admin dengan:
- `ID_Admin`: (auto increment)
- `Nama_Admin`: admin
- `Password`: admin123
- `ID_Ruangan`: NULL
## Login
Setelah data ditambahkan, Anda bisa login dengan:
- **User Type**: Admin (pilih radio button "Admin")
- **Username**: `admin`
- **Password**: `admin123`
## Troubleshooting
### Masalah: "Payload login tidak valid"
1. Pastikan semua field terisi (username dan password tidak kosong)
2. Pastikan backend sudah di-compile ulang setelah perubahan
3. Restart backend server
### Masalah: "Username atau password salah"
1. Verifikasi data admin ada di database:
```bash
go run scripts/check_admin.go
```
2. Pastikan username dan password sesuai (case-sensitive untuk password)
3. Pastikan tidak ada whitespace di username/password
4. Cek log backend untuk error detail
### Masalah: Admin masuk ke dashboard dokter
1. Pastikan memilih radio button "Admin" sebelum login
2. Clear browser cache dan localStorage
3. Pastikan `userRole` di localStorage adalah "admin"
### Masalah: Data admin tidak ditemukan
1. Jalankan script insert admin lagi:
```bash
go run scripts/insert_admin.go
```
2. Verifikasi dengan check script:
```bash
go run scripts/check_admin.go
```
3. Pastikan koneksi database benar di `database/db.go`

View File

@@ -0,0 +1,419 @@
# CareIT Database Views Documentation
## 📋 Overview
Dokumentasi lengkap untuk semua views yang telah ditambahkan ke database CareIT untuk optimasi query dan mempercepat pengaksesan data.
---
## 📊 Daftar Views
### 1. **v_billing_pasien_info**
**Tujuan:** Menampilkan informasi billing pasien dengan data lengkap dan status real-time
**Kolom:**
- `ID_Billing` - ID Billing (PK)
- `ID_Pasien` - ID Pasien (FK)
- `Nama_Pasien` - Nama lengkap pasien
- `Jenis_Kelamin` - Jenis kelamin (Laki-laki/Perempuan)
- `Usia` - Usia pasien
- `Ruangan` - Nama ruangan
- `Kelas` - Kelas perawatan (1, 2, 3)
- `Cara_Bayar` - Metode pembayaran (BPJS/Umum)
- `Tanggal_Masuk` - Tanggal masuk rumah sakit
- `Tanggal_Keluar` - Tanggal keluar (NULL jika masih aktif)
- `Hari_Inap` - Jumlah hari perawatan (calculated)
- `Total_Tarif_RS` - Total tarif RS
- `Total_Klaim` - Total klaim BPJS
- `Billing_Sign` - Status billing (Hijau/Kuning/Merah)
- `Status_Pasien` - Status pasien (Aktif/Selesai)
**Use Case:**
- Dashboard billing pasien
- List view semua pasien
- Filter berdasarkan status atau cara bayar
- Monitoring status pasien aktif
**Query Example:**
```sql
SELECT * FROM v_billing_pasien_info
WHERE Status_Pasien = 'Aktif'
ORDER BY Tanggal_Masuk DESC;
```
---
### 2. **v_billing_detail**
**Tujuan:** Menampilkan detail billing dengan informasi dokter dan tindakan
**Kolom:**
- `ID_Billing` - ID Billing
- `ID_Pasien` - ID Pasien
- `Nama_Pasien` - Nama pasien
- `Cara_Bayar` - Metode pembayaran
- `Tanggal_Masuk` - Tanggal masuk
- `Tanggal_Keluar` - Tanggal keluar
- `Dokter` - Daftar dokter yang menangani
- `KSM` - Kelompok Staf Medis yang terlibat
- `Jumlah_Dokter` - Jumlah dokter yang terlibat
- `Jumlah_Tindakan` - Jumlah tindakan yang dilakukan
- `Total_Tarif_RS` - Total tarif RS
- `Total_Klaim` - Total klaim
- `Billing_Sign` - Status billing
**Use Case:**
- Reporting detail billing
- Tracking dokter per pasien
- Analisis jumlah tindakan per billing
- Verifikasi komposisi tim medis
**Query Example:**
```sql
SELECT * FROM v_billing_detail
WHERE Cara_Bayar = 'BPJS'
AND Jumlah_Dokter >= 2;
```
---
### 3. **v_billing_diagnosis_procedure**
**Tujuan:** Menampilkan diagnosa dan prosedur medis per billing
**Kolom:**
- `ID_Billing` - ID Billing
- `Nama_Pasien` - Nama pasien
- `Cara_Bayar` - Metode pembayaran
- `Kode_Diagnosa` - Daftar kode ICD10 diagnosa
- `Diagnosa` - Daftar diagnosa lengkap
- `Jumlah_Diagnosa` - Jumlah diagnosa
- `Kode_Prosedur` - Daftar kode ICD9 prosedur
- `Prosedur` - Daftar prosedur lengkap
- `Jumlah_Prosedur` - Jumlah prosedur
- `Tanggal_Masuk` - Tanggal masuk
- `Tanggal_Keluar` - Tanggal keluar
**Use Case:**
- Medical record extraction
- Clinical audit trail
- Diagnosis tracking
- Procedure validation
- Export untuk verifikasi medis
**Query Example:**
```sql
SELECT * FROM v_billing_diagnosis_procedure
WHERE Diagnosa LIKE '%A00%';
```
---
### 4. **v_billing_inacbg_code**
**Tujuan:** Menampilkan INACBG code (RI dan RJ) untuk BPJS claim processing
**Kolom:**
- `ID_Billing` - ID Billing
- `Nama_Pasien` - Nama pasien
- `Cara_Bayar` - Metode pembayaran
- `Tipe_Perawatan` - Tipe perawatan (RI=Rawat Inap, RJ=Rawat Jalan)
- `Kode_INACBG_RI` - Daftar kode INACBG RI
- `Kode_INACBG_RJ` - Daftar kode INACBG RJ
- `Jumlah_INACBG_RI` - Jumlah kode RI
- `Jumlah_INACBG_RJ` - Jumlah kode RJ
- `Total_Klaim` - Total klaim
- `Billing_Sign` - Status billing
**Use Case:**
- BPJS claim submission
- INACBG code verification
- Claim tracking
- DRG mapping validation
- Financial reconciliation
**Query Example:**
```sql
SELECT * FROM v_billing_inacbg_code
WHERE Cara_Bayar = 'BPJS'
AND Billing_Sign IN ('Kuning', 'Merah');
```
---
### 5. **v_ruangan_pasien_aktif**
**Tujuan:** Dashboard ruangan dengan occupancy rate dan distribusi pasien per kelas
**Kolom:**
- `ID_Ruangan` - ID Ruangan (PK)
- `Nama_Ruangan` - Nama ruangan
- `Jenis_Ruangan` - Jenis ruangan
- `Kategori_ruangan` - Kategori ruangan
- `Jumlah_Pasien_Aktif` - Total pasien aktif di ruangan
- `Pasien_Kelas_1` - Jumlah pasien kelas 1
- `Pasien_Kelas_2` - Jumlah pasien kelas 2
- `Pasien_Kelas_3` - Jumlah pasien kelas 3
- `Nama_Pasien` - Daftar nama pasien aktif
**Use Case:**
- Real-time occupancy dashboard
- Room management
- Patient distribution analysis
- Capacity planning
- Class-based tracking
**Query Example:**
```sql
SELECT * FROM v_ruangan_pasien_aktif
WHERE Jumlah_Pasien_Aktif > 0
ORDER BY Jumlah_Pasien_Aktif DESC;
```
---
### 6. **v_dokter_billing_stat**
**Tujuan:** Statistik kinerja dokter dengan tracking billing dan klaim
**Kolom:**
- `ID_Dokter` - ID Dokter (PK)
- `Nama_Dokter` - Nama dokter
- `Status` - Status (DPJP/PPDS)
- `KSM` - Kelompok Staf Medis
- `Jumlah_Billing` - Jumlah billing yang ditangani
- `Jumlah_Pasien` - Jumlah pasien unik
- `Total_Klaim` - Total klaim dari semua billing
- `Tanggal_Pasien_Terakhir` - Tanggal pasien terakhir ditangani
- `Tipe_Pasien` - Tipe pasien yang ditangani (BPJS/Umum)
**Use Case:**
- Dokter performance dashboard
- Workload analysis
- Billing tracking per dokter
- KSM comparative analysis
- Productivity metrics
**Query Example:**
```sql
SELECT * FROM v_dokter_billing_stat
WHERE Jumlah_Billing > 5
ORDER BY Total_Klaim DESC;
```
---
### 7. **v_pasien_billing_history**
**Tujuan:** Riwayat lengkap pasien dengan semua billing dan klaim
**Kolom:**
- `ID_Pasien` - ID Pasien (PK)
- `Nama_Pasien` - Nama lengkap pasien
- `Jenis_Kelamin` - Jenis kelamin
- `Usia` - Usia pasien
- `Ruangan` - Ruangan tempat dirawat
- `Jumlah_Billing` - Total billing sepanjang waktu
- `Jumlah_Billing_Aktif` - Billing yang masih aktif
- `Jumlah_Billing_Selesai` - Billing yang sudah selesai
- `Total_Klaim_Keseluruhan` - Total klaim keseluruhan
- `Tanggal_Masuk_Terakhir` - Tanggal masuk terakhir
- `Tanggal_Keluar_Terakhir` - Tanggal keluar terakhir
- `Riwayat_Cara_Bayar` - Riwayat cara pembayaran
**Use Case:**
- Patient medical history
- Complete patient profile
- Historical billing analysis
- Treatment continuity tracking
- Patient lifetime value analysis
**Query Example:**
```sql
SELECT * FROM v_pasien_billing_history
WHERE Jumlah_Billing > 1
ORDER BY Total_Klaim_Keseluruhan DESC;
```
---
### 8. **v_billing_summary_harian**
**Tujuan:** Summary harian billing untuk operational dashboard
**Kolom:**
- `Tanggal` - Tanggal (DATE)
- `Jumlah_Billing_Masuk` - Jumlah billing masuk hari tersebut
- `Billing_Keluar_Hari_Sama` - Billing keluar di hari yang sama
- `Billing_Aktif` - Billing yang masih aktif (tidak keluar)
- `Status_Hijau` - Jumlah billing dengan status Hijau
- `Status_Kuning` - Jumlah billing dengan status Kuning
- `Status_Merah` - Jumlah billing dengan status Merah
- `Total_Tarif_RS_Harian` - Total tarif RS harian
- `Total_Klaim_Harian` - Total klaim harian
- `Tipe_Pasien_Masuk` - Tipe pasien yang masuk (BPJS/Umum)
**Use Case:**
- Daily operational report
- Real-time monitoring dashboard
- Hospital KPI tracking
- Revenue analysis
- Status distribution monitoring
**Query Example:**
```sql
SELECT * FROM v_billing_summary_harian
WHERE Tanggal BETWEEN DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND CURDATE()
ORDER BY Tanggal DESC;
```
---
### 9. **v_billing_tarif_analysis**
**Tujuan:** Analisis detail tarif dan klaim untuk financial validation
**Kolom:**
- `ID_Billing` - ID Billing
- `Nama_Pasien` - Nama pasien
- `Cara_Bayar` - Metode pembayaran
- `Total_Tarif_RS` - Total tarif RS
- `Total_Klaim` - Total klaim
- `Selisih_Tarif_Klaim` - Selisih tarif dan klaim
- `Persentase_Klaim` - Persentase klaim terhadap tarif (%)
- `Billing_Sign` - Status billing
- `Jumlah_Tindakan` - Jumlah tindakan
- `Jumlah_Kode_INACBG` - Jumlah kode INACBG
**Use Case:**
- Financial audit
- Tarif vs claim analysis
- Billing accuracy validation
- Revenue reconciliation
- Claim percentage tracking
**Query Example:**
```sql
SELECT * FROM v_billing_tarif_analysis
WHERE Persentase_Klaim < 50
AND Billing_Sign = 'Merah';
```
---
### 10. **v_ksm_performance**
**Tujuan:** Performance metrics per KSM (Kelompok Staf Medis)
**Kolom:**
- `KSM` - Nama KSM (PK)
- `Jumlah_Dokter` - Jumlah dokter di KSM
- `Jumlah_Billing_Ditangani` - Total billing yang ditangani
- `Avg_Billing_Per_Dokter` - Rata-rata billing per dokter
- `Total_Klaim_KSM` - Total klaim keseluruhan KSM
- `Billing_Sign_Hijau` - Jumlah billing status Hijau
- `Billing_Sign_Kuning` - Jumlah billing status Kuning
- `Billing_Sign_Merah` - Jumlah billing status Merah
**Use Case:**
- Departmental performance analysis
- Comparative KSM metrics
- Resource allocation planning
- Quality metrics tracking
- Financial performance by department
**Query Example:**
```sql
SELECT * FROM v_ksm_performance
ORDER BY Total_Klaim_KSM DESC;
```
---
## 🔍 Index Recommendation
Untuk optimasi lebih lanjut, recommend untuk membuat index pada kolom yang sering digunakan:
```sql
-- Index untuk v_billing_pasien_info
CREATE INDEX idx_billing_pasien_status ON billing_pasien(Tanggal_Keluar, Billing_Sign);
CREATE INDEX idx_billing_pasien_cara_bayar ON billing_pasien(Cara_Bayar);
CREATE INDEX idx_pasien_ruangan ON pasien(Ruangan);
-- Index untuk v_billing_detail
CREATE INDEX idx_billing_dokter_id ON billing_dokter(ID_Billing, ID_Dokter);
CREATE INDEX idx_dokter_ksm ON dokter(KSM);
-- Index untuk v_billing_diagnosis_procedure
CREATE INDEX idx_billing_icd10 ON billing_icd10(ID_Billing);
CREATE INDEX idx_billing_icd9 ON billing_icd9(ID_Billing);
-- Index untuk v_billing_inacbg_code
CREATE INDEX idx_billing_inacbg_ri ON billing_inacbg_ri(ID_Billing);
CREATE INDEX idx_billing_inacbg_rj ON billing_inacbg_rj(ID_Billing);
-- Index untuk tanggal
CREATE INDEX idx_billing_tanggal_masuk ON billing_pasien(Tanggal_Masuk);
CREATE INDEX idx_billing_tanggal_keluar ON billing_pasien(Tanggal_Keluar);
```
---
## 📝 Notes & Best Practices
### ✅ Kelebihan menggunakan Views:
1. **Performa Lebih Cepat** - Query sudah pre-compiled
2. **Konsistensi Data** - Logika aggregation terpusat
3. **Kemudahan Maintenance** - Perubahan logic hanya di satu tempat
4. **Security** - Bisa membatasi akses ke kolom tertentu
5. **Abstraksi** - Frontend tidak perlu tahu struktur table kompleks
### ⚠️ Perhatian:
1. Views adalah **read-only** (SELECT only) di versi MariaDB ini
2. Performa depends pada database size - gunakan index yang tepat
3. Aggregate functions (COUNT, SUM, etc) bisa lambat untuk dataset besar
4. Refresh view dengan menjalankan EXPLAIN untuk cek query plan
### 🚀 Tips Optimasi:
1. Gunakan WHERE clause untuk filter sebanyak mungkin
2. Limit hasil jika tidak perlu semua data
3. Cache hasil di aplikasi jika data tidak berubah sering
4. Monitor query performance dengan EXPLAIN
5. Update index statistics secara berkala
---
## 📊 Contoh Penggunaan di Backend
### Go Example (menggunakan GORM):
```go
// Model untuk View
type BillingPasienInfo struct {
IDBilling int `gorm:"column:ID_Billing"`
IDPasien int `gorm:"column:ID_Pasien"`
NamaPasien string `gorm:"column:Nama_Pasien"`
CaraBayar string `gorm:"column:Cara_Bayar"`
HariInap int `gorm:"column:Hari_Inap"`
StatusPasien string `gorm:"column:Status_Pasien"`
}
func (BillingPasienInfo) TableName() string {
return "v_billing_pasien_info"
}
// Usage dalam handler
func GetBillingAktif(db *gorm.DB, c *gin.Context) {
var billings []BillingPasienInfo
db.Where("Status_Pasien = ?", "Aktif").
Order("Tanggal_Masuk DESC").
Find(&billings)
c.JSON(200, billings)
}
```
---
## 🔄 Update & Maintenance
Views akan **secara otomatis** updated ketika data di table yang di-reference berubah. Tidak perlu maintenance manual.
---
**Dokumentasi dibuat: 23 Desember 2025**
**Database: CareIT v2**
**Version: 1.0**

View File

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"
}
}
}

View File

@@ -0,0 +1,225 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Billing INACBG</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container-fluid">
<div class="row h-100">
<!-- Sidebar -->
<div class="col-md-3 sidebar">
<div class="sidebar-header">
<h5>Ruangan</h5>
</div>
<div class="ruangan-list" id="ruanganList">
<!-- Will be populated by JS -->
</div>
</div>
<!-- Main Content -->
<div class="col-md-9 main-content">
<!-- Header -->
<div class="header">
<div class="d-flex justify-content-between align-items-center">
<h2>Data Billing Pasien</h2>
<div class="text-muted small" id="currentDate"></div>
</div>
<div class="search-box mt-3">
<input type="text" id="searchInput" class="form-control" placeholder="Cari billing pasien dian">
</div>
</div>
<!-- Billing Table -->
<div class="billing-table-container">
<table class="table table-hover">
<thead>
<tr>
<th>ID Pasien</th>
<th>Nama</th>
<th>Total Tarif RS</th>
<th>Total Klaim BPJS</th>
<th>Billing Sign</th>
<th>Action</th>
</tr>
</thead>
<tbody id="billingTableBody">
<!-- Will be populated by JS -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Data Pasien</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Patient Info Section -->
<div class="mb-4">
<h6 class="text-secondary">Nama Lengkap</h6>
<input type="text" id="modalNamaPasien" class="form-control" readonly>
</div>
<div class="row mb-4">
<div class="col-md-6">
<h6 class="text-secondary">ID Pasien</h6>
<input type="text" id="modalIdPasien" class="form-control" readonly>
</div>
<div class="col-md-6">
<h6 class="text-secondary">Kelas</h6>
<input type="text" id="modalKelas" class="form-control" readonly>
</div>
</div>
<!-- Dokter yang Menangani -->
<div class="mb-4">
<h6 class="text-secondary fw-bold">Dokter yang Menangani</h6>
<div id="dokterList" class="border rounded p-2 bg-light small">
<span class="text-muted">Memuat data dokter...</span>
</div>
</div>
<!-- Tindakan & ICD - Pisah Lama vs Baru -->
<div class="mb-4">
<h6 class="text-secondary fw-bold">Tindakan RS</h6>
<div class="row">
<div class="col-md-6">
<small class="text-muted">Data yang sudah ada:</small>
<div id="tindakanLama" class="border rounded p-2 bg-light small"></div>
</div>
<div class="col-md-6">
<small class="text-muted">Data baru (akan ditambahkan):</small>
<div id="tindakanBaru" class="border rounded p-2 bg-light small text-muted">Belum ada data baru</div>
</div>
</div>
</div>
<div class="mb-4">
<h6 class="text-secondary fw-bold">ICD 9 & ICD 10</h6>
<div class="row">
<div class="col-md-6">
<small class="text-muted">ICD 9 - Data yang sudah ada:</small>
<div id="icd9Lama" class="border rounded p-2 bg-light small mb-2"></div>
<small class="text-muted">ICD 10 - Data yang sudah ada:</small>
<div id="icd10Lama" class="border rounded p-2 bg-light small"></div>
</div>
<div class="col-md-6">
<small class="text-muted">ICD 9 - Data baru:</small>
<div id="icd9Baru" class="border rounded p-2 bg-light small mb-2 text-muted">Belum ada data baru</div>
<small class="text-muted">ICD 10 - Data baru:</small>
<div id="icd10Baru" class="border rounded p-2 bg-light small text-muted">Belum ada data baru</div>
</div>
</div>
</div>
<div class="mb-4">
<h6 class="text-secondary">Total Tarif RS (Kumulatif)</h6>
<input type="text" id="modalTotalTarif" class="form-control" readonly>
</div>
<!-- INACBG Form -->
<div class="mb-4">
<h6 class="text-secondary">INA CBG</h6>
<form id="inacbgForm">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Tipe INACBG</label>
<select id="tipeInacbg" class="form-select">
<option value="">-- Pilih Tipe --</option>
<option value="RI">RI (Rawat Inap)</option>
<option value="RJ">RJ (Rawat Jalan)</option>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Masukkan Kode INA CBGS</label>
<div class="input-group">
<select id="inacbgCode" class="form-select" disabled>
<option value="">-- Pilih Tipe INACBG Dulu --</option>
</select>
<span class="input-group-text" style="cursor: pointer; user-select: none;" title="Ganti ke input manual" onclick="toggleInacbgInput()">
↔️
</span>
</div>
<input type="text" id="inacbgCodeManual" class="form-control d-none mt-2" placeholder="Ketik kode INACBG manual">
</div>
<div class="col-md-6 d-flex align-items-end">
<button type="button" id="addCodeBtn" class="btn btn-primary w-100">+</button>
</div>
</div>
<!-- INACBG Lama -->
<div class="mb-3">
<label class="form-label fw-bold">INACBG yang sudah ada sebelumnya:</label>
<div id="inacbgLamaContainer" class="border rounded p-2 bg-light small">
<div id="inacbgRILama" class="mb-1"></div>
<div id="inacbgRJLama"></div>
<div id="totalKlaimLama" class="mt-2 fw-bold"></div>
</div>
</div>
<!-- INACBG Baru -->
<div class="mb-3">
<label class="form-label fw-bold">INACBG Baru (akan ditambahkan):</label>
<div id="codeList" class="border rounded p-2 bg-light">
<small class="text-muted">Belum ada kode baru</small>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<label class="form-label">Total Klaim Lama</label>
<input type="number" id="totalKlaimLamaInput" class="form-control" placeholder="0" readonly>
</div>
<div class="col-md-4">
<label class="form-label">Total Klaim Baru <span class="text-muted small">(Otomatis)</span></label>
<input type="number" id="totalKlaim" class="form-control" placeholder="0" readonly>
</div>
<div class="col-md-4">
<label class="form-label">Total Klaim Akhir</label>
<input type="number" id="totalKlaimAkhir" class="form-control fw-bold" placeholder="0" readonly>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Billing Sign</label>
<div id="billingSignContainer" class="mt-1">
<span id="billingSignBadge" class="badge bg-secondary">-</span>
<span id="billingSignText" class="ms-2 text-muted small"></span>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Tanggal Keluar</label>
<input type="date" id="tanggalKeluar" class="form-control">
</div>
</div>
<div id="formAlert" class="alert d-none" role="alert"></div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-success">Save</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="scriptAdmin.js"></script>
</body>
</html>

View File

@@ -0,0 +1,158 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Billing INACBG</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container-fluid">
<div class="row h-100">
<!-- Sidebar -->
<div class="col-md-3 sidebar">
<div class="sidebar-header">
<h5>Ruangan</h5>
</div>
<div class="ruangan-list" id="ruanganList">
<!-- Will be populated by JS -->
</div>
</div>
<!-- Main Content -->
<div class="col-md-9 main-content">
<!-- Header -->
<div class="header">
<div class="d-flex justify-content-between align-items-center">
<h2>Data Billing Pasien</h2>
<div class="text-muted small" id="currentDate"></div>
</div>
<div class="search-box mt-3">
<input type="text" id="searchInput" class="form-control" placeholder="Cari billing pasien dian">
</div>
</div>
<!-- Billing Table -->
<div class="billing-table-container">
<table class="table table-hover">
<thead>
<tr>
<th>ID Pasien</th>
<th>Nama</th>
<th>Billing Sign</th>
<th>Action</th>
</tr>
</thead>
<tbody id="billingTableBody">
<!-- Will be populated by JS -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Data Pasien</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Patient Info Section -->
<div class="mb-4">
<h6 class="text-secondary">Nama Lengkap</h6>
<input type="text" id="modalNamaPasien" class="form-control" readonly>
</div>
<div class="row mb-4">
<div class="col-md-6">
<h6 class="text-secondary">ID Pasien</h6>
<input type="text" id="modalIdPasien" class="form-control" readonly>
</div>
<div class="col-md-6">
<h6 class="text-secondary">Kelas</h6>
<input type="text" id="modalKelas" class="form-control" readonly>
</div>
</div>
<!-- Tindakan & Pemeriksaan -->
<div class="mb-4">
<h6 class="text-secondary">Tindakan dan Pemeriksaan Penunjang</h6>
<input type="text" id="modalTindakan" class="form-control" readonly placeholder="(diambil dari billing)">
</div>
<div class="mb-4">
<h6 class="text-secondary">Total Tarif RS</h6>
<input type="text" id="modalTotalTarif" class="form-control" readonly>
</div>
<!-- ICD Codes -->
<div class="row mb-4">
<div class="col-md-6">
<h6 class="text-secondary">ICD 9</h6>
<input type="text" id="modalICD9" class="form-control" readonly>
</div>
<div class="col-md-6">
<h6 class="text-secondary">ICD 10</h6>
<input type="text" id="modalICD10" class="form-control" readonly>
</div>
</div>
<!-- INACBG Form -->
<div class="mb-4">
<h6 class="text-secondary">INA CBG</h6>
<form id="inacbgForm">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Tipe INACBG</label>
<select id="tipeInacbg" class="form-select">
<option value="">-- Pilih Tipe --</option>
<option value="RI">RI (Rawat Inap)</option>
<option value="RJ">RJ (Rawat Jalan)</option>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Masukkan Kode INA CBGS</label>
<select id="inacbgCode" class="form-select" disabled>
<option value="">-- Pilih Tipe INACBG Dulu --</option>
</select>
</div>
<div class="col-md-6 d-flex align-items-end">
<button type="button" id="addCodeBtn" class="btn btn-primary">+</button>
</div>
</div>
<div id="codeList" class="mb-3">
<!-- Added codes will appear here -->
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Total Klaim BJPS <span class="text-muted small">(Otomatis)</span></label>
<input type="number" id="totalKlaim" class="form-control" placeholder="0" readonly>
</div>
</div>
<div id="formAlert" class="alert d-none" role="alert"></div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-success">Save</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="scriptAdmin.js"></script>
</body>
</html>

View File

@@ -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);
});
}

View File

@@ -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);
});
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,199 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Form Billing Pasien BPJS</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<div class="card shadow-lg">
<div class="card-header">
<h2 class="mb-0 text-white">Form Billing Pasien BPJS</h2>
</div>
<div class="card-body">
<form id="bpjsForm">
<!-- === DOKTER === -->
<div class="mb-3">
<label class="form-label">Nama Dokter</label>
<div class="searchable-select-wrapper" id="wrapper_nama_dokter">
<input type="text" class="searchable-select-input" id="nama_dokter" readonly placeholder="Dokter...">
<span class="searchable-select-arrow"></span>
<div class="searchable-select-dropdown" id="dropdown_nama_dokter">
<div class="searchable-select-search">
<input type="text" placeholder="Cari..." id="search_nama_dokter" autocomplete="off">
</div>
<div class="searchable-select-options" id="options_nama_dokter"></div>
</div>
</div>
<select class="form-select d-none" id="select_nama_dokter" name="nama_dokter"></select>
</div>
<!-- NAMA PASIEN -->
<div class="mb-3 position-relative">
<label class="form-label">Nama Pasien</label>
<input type="text" class="form-control" id="nama_pasien" autocomplete="off">
<div id="list_pasien" class="autocomplete-list"></div>
<input type="hidden" id="id_pasien" name="id_pasien">
</div>
<!-- Auto Fill atau Manual -->
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Jenis Kelamin</label>
<input type="text" class="form-control" id="jenis_kelamin" placeholder="Auto fill atau isi manual">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Usia</label>
<input type="number" class="form-control" id="usia" placeholder="Auto fill atau isi manual">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Ruangan</label>
<div class="searchable-select-wrapper" id="wrapper_ruangan">
<input type="text" class="searchable-select-input" id="ruangan" readonly placeholder="-- Pilih Ruangan --">
<span class="searchable-select-arrow"></span>
<div class="searchable-select-dropdown" id="dropdown_ruangan">
<div class="searchable-select-search">
<input type="text" placeholder="Cari..." id="search_ruangan" autocomplete="off">
</div>
<div class="searchable-select-options" id="options_ruangan"></div>
</div>
</div>
<select class="form-select d-none" id="select_ruangan" name="ruangan"></select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Kelas</label>
<select class="form-select" id="kelas" name="kelas">
<option value="">-- Pilih Kelas --</option>
<option value="1">Kelas 1</option>
<option value="2">Kelas 2</option>
<option value="3">Kelas 3</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Cara Bayar</label>
<select class="form-select" id="cara_bayar" name="cara_bayar">
<option value="">-- Pilih Cara Bayar --</option>
<option value="BPJS">BPJS</option>
<option value="UMUM">Umum</option>
</select>
</div>
<!-- Tanggal Keluar diisi oleh Admin Billing, bukan di form dokter -->
</div>
<!-- TINDAKAN -->
<div class="mb-3">
<label class="form-label">Tindakan RS</label>
<div class="searchable-select-wrapper multi-select" id="wrapper_tarif_rs">
<div class="searchable-select-input" id="tarif_rs" placeholder="-- Pilih --">
<input type="text" id="input_tarif_rs" placeholder="Tindakan..." autocomplete="off">
</div>
<span class="searchable-select-arrow"></span>
<div class="searchable-select-dropdown" id="dropdown_tarif_rs">
<div class="searchable-select-search">
<input type="text" placeholder="Cari..." id="search_tarif_rs" autocomplete="off">
</div>
<div class="searchable-select-options" id="options_tarif_rs"></div>
</div>
</div>
<select class="form-select d-none" id="select_tarif_rs" name="tarif_rs" multiple></select>
</div>
<!-- ICD -->
<div class="mb-3">
<label class="form-label">ICD 9</label>
<div class="searchable-select-wrapper multi-select" id="wrapper_icd9">
<div class="searchable-select-input" id="icd9" placeholder="-- Pilih --">
<input type="text" id="input_icd9" placeholder="-- Pilih --" autocomplete="off">
</div>
<span class="searchable-select-arrow"></span>
<div class="searchable-select-dropdown" id="dropdown_icd9">
<div class="searchable-select-search">
<input type="text" placeholder="Cari..." id="search_icd9" autocomplete="off">
</div>
<div class="searchable-select-options" id="options_icd9"></div>
</div>
</div>
<select class="form-select d-none" id="select_icd9" name="icd9" multiple></select>
</div>
<div class="mb-3">
<label class="form-label">ICD 10</label>
<div class="searchable-select-wrapper multi-select" id="wrapper_icd10">
<div class="searchable-select-input" id="icd10" placeholder="-- Pilih --">
<input type="text" id="input_icd10" placeholder="-- Pilih --" autocomplete="off">
</div>
<span class="searchable-select-arrow"></span>
<div class="searchable-select-dropdown" id="dropdown_icd10">
<div class="searchable-select-search">
<input type="text" placeholder="Cari..." id="search_icd10" autocomplete="off">
</div>
<div class="searchable-select-options" id="options_icd10"></div>
</div>
</div>
<select class="form-select d-none" id="select_icd10" name="icd10" multiple></select>
</div>
<!-- RIWAYAT BILLING AKTIF (TINDAKAN & ICD SEBELUMNYA) -->
<div class="mb-4">
<label class="form-label fw-bold">Riwayat Tindakan & ICD (Billing Aktif)</label>
<div id="billing_history_info" class="small text-muted mb-2">
Belum ada data yang dimuat. Pilih pasien untuk melihat riwayat.
</div>
<div class="row">
<div class="col-md-4">
<h6 class="fw-semibold">Tindakan RS</h6>
<ul id="history_tindakan_rs" class="list-group small"></ul>
</div>
<div class="col-md-4">
<h6 class="fw-semibold">ICD 9</h6>
<ul id="history_icd9" class="list-group small"></ul>
</div>
<div class="col-md-4">
<h6 class="fw-semibold">ICD 10</h6>
<ul id="history_icd10" class="list-group small"></ul>
</div>
</div>
</div>
<!-- TOTAL TARIF RS -->
<div class="mb-3">
<label class="form-label">Total Tarif RS</label>
<div class="input-group">
<span class="input-group-text">Rp</span>
<input type="text" class="form-control" id="total_tarif_rs" name="total_tarif_rs" readonly placeholder="0">
</div>
<small class="text-muted">Total akan dihitung otomatis berdasarkan tindakan yang dipilih</small>
</div>
<div id="formAlert" class="alert d-none" role="alert"></div>
<div class="d-grid gap-2 mt-4">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary btn-lg">
💾 Simpan Data
</button>
<button type="button" id="saveDraftBtn" class="btn btn-outline-secondary btn-lg">💾 Save Draft</button>
<button type="button" id="clearDraftBtn" class="btn btn-outline-danger btn-lg">🗑️ Clear Draft</button>
<div id="draftStatus" class="align-self-center ms-2 text-muted" style="font-size:0.95rem"></div>
</div>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- App script: loads dropdowns, autocomplete and form handlers -->
<script src="script.js"></script>
</body>
</html>

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}
}

View File

@@ -0,0 +1,789 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Backend Careit - Test Interface</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 1.1em;
}
.content {
padding: 30px;
}
.section {
margin-bottom: 30px;
border: 1px solid #e0e0e0;
border-radius: 10px;
overflow: hidden;
}
.section-header {
background: #f5f5f5;
padding: 15px 20px;
font-weight: bold;
font-size: 1.2em;
color: #333;
border-bottom: 2px solid #667eea;
}
.section-content {
padding: 20px;
}
.endpoint-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.endpoint-item {
display: flex;
flex-direction: column;
}
.endpoint-item label {
font-weight: 600;
margin-bottom: 5px;
color: #555;
font-size: 0.9em;
}
.endpoint-item input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 0.95em;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 25px;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
font-weight: 600;
transition: transform 0.2s, box-shadow 0.2s;
margin-top: 10px;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
.btn-list {
background: #4CAF50;
}
.btn-list:hover {
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
}
.response-area {
margin-top: 20px;
}
.response-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.response-header h3 {
color: #333;
}
.status-badge {
padding: 5px 15px;
border-radius: 20px;
font-size: 0.85em;
font-weight: 600;
}
.status-ok {
background: #4CAF50;
color: white;
}
.status-error {
background: #f44336;
color: white;
}
.status-loading {
background: #ff9800;
color: white;
}
.view-toggle {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.view-toggle button {
padding: 8px 16px;
font-size: 0.9em;
}
.view-toggle button.active {
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9), 0 5px 15px rgba(102, 126, 234, 0.4);
transform: translateY(-1px);
}
#response,
#tableResponse {
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
max-height: 500px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 0.9em;
white-space: pre-wrap;
word-wrap: break-word;
}
#tableResponse {
display: none;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: white;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9em;
}
thead {
background: #667eea;
color: white;
position: sticky;
top: 0;
z-index: 1;
}
th, td {
padding: 8px 10px;
border: 1px solid #eee;
text-align: left;
}
tbody tr:nth-child(even) {
background: #fafafa;
}
tbody tr:hover {
background: #f1f5ff;
}
.health-check {
text-align: center;
padding: 20px;
background: #e8f5e9;
border-radius: 10px;
margin-bottom: 30px;
}
.health-status {
font-size: 1.5em;
font-weight: bold;
margin: 10px 0;
}
.health-ok {
color: #4CAF50;
}
.health-error {
color: #f44336;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🏥 Backend Careit</h1>
<p>Test Interface untuk API Backend</p>
</div>
<div class="content">
<!-- Health Check -->
<div class="health-check">
<h2>Health Check</h2>
<button onclick="checkHealth()" style="margin-top: 10px;">Cek Status Server</button>
<div id="healthStatus" class="health-status"></div>
</div>
<!-- Tarif BPJS Rawat Inap -->
<div class="section">
<div class="section-header">📋 Tarif BPJS Rawat Inap</div>
<div class="section-content">
<div class="endpoint-group">
<div class="endpoint-item">
<button class="btn-list" onclick="getTarifBPJSRawatInap()">List Semua Tarif</button>
</div>
<div class="endpoint-item">
<label>Kode INA CBG:</label>
<input type="text" id="kodeRawatInap" placeholder="Masukkan kode...">
<button onclick="getTarifBPJSRawatInapByKode()">Cari by Kode</button>
</div>
</div>
</div>
</div>
<!-- Tarif BPJS Rawat Jalan -->
<div class="section">
<div class="section-header">🚶 Tarif BPJS Rawat Jalan</div>
<div class="section-content">
<div class="endpoint-group">
<div class="endpoint-item">
<button class="btn-list" onclick="getTarifBPJSRawatJalan()">List Semua Tarif</button>
</div>
<div class="endpoint-item">
<label>Kode INA CBG:</label>
<input type="text" id="kodeRawatJalan" placeholder="Masukkan kode...">
<button onclick="getTarifBPJSRawatJalanByKode()">Cari by Kode</button>
</div>
</div>
</div>
</div>
<!-- Tarif RS -->
<div class="section">
<div class="section-header">🏨 Tarif RS</div>
<div class="section-content">
<div class="endpoint-group">
<div class="endpoint-item">
<button class="btn-list" onclick="getTarifRS()">List Semua Tarif</button>
</div>
<div class="endpoint-item">
<label>Kode RS:</label>
<input type="text" id="kodeRS" placeholder="Masukkan kode...">
<button onclick="getTarifRSByKode()">Cari by Kode</button>
</div>
<div class="endpoint-item">
<label>Kategori:</label>
<input type="text" id="kategoriRS" placeholder="Masukkan kategori...">
<button onclick="getTarifRSByKategori()">Cari by Kategori</button>
</div>
</div>
</div>
</div>
<!-- ICD9 -->
<div class="section">
<div class="section-header">🧬 Kode Tindakan ICD9</div>
<div class="section-content">
<div class="endpoint-group">
<div class="endpoint-item">
<button class="btn-list" onclick="getICD9()">List Semua Kode ICD9</button>
</div>
<div class="endpoint-item">
<label>Filter (client side, berdasarkan teks):</label>
<input type="text" id="filterICD9" placeholder="Ketik untuk filter di tabel..." oninput="filterICD9ClientSide()">
</div>
</div>
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
Data diambil dari endpoint <code>/icd9</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON (misal: kode, deskripsi, dll).
</p>
</div>
</div>
<!-- ICD10 -->
<div class="section">
<div class="section-header">🩺 Kode Diagnosis ICD10</div>
<div class="section-content">
<div class="endpoint-group">
<div class="endpoint-item">
<button class="btn-list" onclick="getICD10()">List Semua Kode ICD10</button>
</div>
<div class="endpoint-item">
<label>Filter (client side, berdasarkan teks):</label>
<input type="text" id="filterICD10" placeholder="Ketik untuk filter di tabel..." oninput="filterICD10ClientSide()">
</div>
</div>
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
Data diambil dari endpoint <code>/icd10</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON.
</p>
</div>
</div>
<!-- Dokter -->
<div class="section">
<div class="section-header">👨‍⚕️ Data Dokter</div>
<div class="section-content">
<div class="endpoint-group">
<div class="endpoint-item">
<button class="btn-list" onclick="getDokter()">List Semua Dokter</button>
</div>
<div class="endpoint-item">
<label>Filter (client side, berdasarkan teks):</label>
<input type="text" id="filterDokter" placeholder="Ketik untuk filter di tabel..." oninput="filterDokterClientSide()">
</div>
</div>
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
Data diambil dari endpoint <code>/dokter</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON.
</p>
</div>
</div>
<!-- Ruangan -->
<div class="section">
<div class="section-header">🏥 Data Ruangan</div>
<div class="section-content">
<div class="endpoint-group">
<div class="endpoint-item">
<button class="btn-list" onclick="getRuangan()">List Semua Ruangan</button>
</div>
<div class="endpoint-item">
<label>Filter (client side, berdasarkan teks):</label>
<input type="text" id="filterRuangan" placeholder="Ketik untuk filter di tabel..." oninput="filterRuanganClientSide()">
</div>
</div>
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
Data diambil dari endpoint <code>/ruangan</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON.
</p>
</div>
</div>
<!-- Response Area -->
<div class="response-area">
<div class="response-header">
<h3>Response:</h3>
<span id="statusBadge" class="status-badge" style="display: none;"></span>
</div>
<div class="view-toggle">
<button id="btnJsonView" class="active" onclick="setViewMode('json')">JSON</button>
<button id="btnTableView" onclick="setViewMode('table')">Table (Auto)</button>
</div>
<div id="response"></div>
<div id="tableResponse"></div>
</div>
</div>
</div>
<script>
const API_BASE = 'http://localhost:8081';
function showResponse(data, status = 'ok') {
const responseDiv = document.getElementById('response');
const tableDiv = document.getElementById('tableResponse');
const statusBadge = document.getElementById('statusBadge');
// Tampilkan JSON mentah
responseDiv.textContent = JSON.stringify(data, null, 2);
// Coba render tabel
renderTable(data);
statusBadge.style.display = 'inline-block';
statusBadge.className = 'status-badge status-' + status;
statusBadge.textContent = status === 'ok' ? '✓ Success' : status === 'loading' ? '⏳ Loading...' : '✗ Error';
}
function setViewMode(mode) {
const responseDiv = document.getElementById('response');
const tableDiv = document.getElementById('tableResponse');
const btnJson = document.getElementById('btnJsonView');
const btnTable = document.getElementById('btnTableView');
if (mode === 'json') {
responseDiv.style.display = 'block';
tableDiv.style.display = 'none';
btnJson.classList.add('active');
btnTable.classList.remove('active');
} else {
responseDiv.style.display = 'none';
tableDiv.style.display = 'block';
btnJson.classList.remove('active');
btnTable.classList.add('active');
}
}
function formatRupiah(value) {
if (value === null || value === undefined || value === '') {
return '';
}
// Convert to number
const num = typeof value === 'string' ? parseFloat(value) : value;
if (isNaN(num)) {
return value;
}
// Format dengan pemisah ribuan
return 'Rp ' + num.toLocaleString('id-ID', {
minimumFractionDigits: 0,
maximumFractionDigits: 2
});
}
function isTarifColumn(key) {
const lowerKey = key.toLowerCase();
return lowerKey.includes('tarif') ||
lowerKey.includes('harga') ||
lowerKey.includes('kelas') ||
lowerKey.includes('tarif_inacbg');
}
function renderTable(data) {
const tableDiv = document.getElementById('tableResponse');
// Reset isi
tableDiv.innerHTML = '';
// Hanya render tabel untuk array of objects
if (!Array.isArray(data) || data.length === 0 || typeof data[0] !== 'object') {
setViewMode('json');
return;
}
const keys = Array.from(
data.reduce((set, item) => {
Object.keys(item || {}).forEach(k => set.add(k));
return set;
}, new Set())
);
if (keys.length === 0) {
setViewMode('json');
return;
}
const table = document.createElement('table');
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
const headerRow = document.createElement('tr');
keys.forEach(key => {
const th = document.createElement('th');
th.textContent = key;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
data.forEach(row => {
const tr = document.createElement('tr');
keys.forEach(key => {
const td = document.createElement('td');
const value = row && key in row ? row[key] : '';
// Format tarif dengan rupiah
if (isTarifColumn(key) && (typeof value === 'number' || (typeof value === 'string' && !isNaN(value) && value !== ''))) {
td.textContent = formatRupiah(value);
td.style.textAlign = 'right';
} else {
td.textContent = typeof value === 'object' ? JSON.stringify(value) : value;
}
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(thead);
table.appendChild(tbody);
tableDiv.appendChild(table);
// Auto switch ke view tabel
setViewMode('table');
}
async function checkHealth() {
try {
showResponse('Loading...', 'loading');
const response = await fetch(`${API_BASE}/`);
const data = await response.json();
const healthStatus = document.getElementById('healthStatus');
if (response.ok) {
healthStatus.textContent = '✓ Server Online';
healthStatus.className = 'health-status health-ok';
} else {
healthStatus.textContent = '✗ Server Error';
healthStatus.className = 'health-status health-error';
}
showResponse(data, response.ok ? 'ok' : 'error');
} catch (error) {
const healthStatus = document.getElementById('healthStatus');
healthStatus.textContent = '✗ Connection Error';
healthStatus.className = 'health-status health-error';
showResponse({ error: error.message }, 'error');
}
}
async function getTarifBPJSRawatInap() {
try {
showResponse('Loading...', 'loading');
const response = await fetch(`${API_BASE}/tarifBPJSRawatInap`);
const data = await response.json();
showResponse(data, response.ok ? 'ok' : 'error');
} catch (error) {
showResponse({ error: error.message }, 'error');
}
}
async function getTarifBPJSRawatInapByKode() {
const kode = document.getElementById('kodeRawatInap').value;
if (!kode) {
alert('Silakan masukkan kode!');
return;
}
try {
showResponse('Loading...', 'loading');
const response = await fetch(`${API_BASE}/tarifBPJS/${encodeURIComponent(kode)}`);
const data = await response.json();
showResponse(data, response.ok ? 'ok' : 'error');
} catch (error) {
showResponse({ error: error.message }, 'error');
}
}
async function getTarifBPJSRawatJalan() {
try {
showResponse('Loading...', 'loading');
const response = await fetch(`${API_BASE}/tarifBPJSRawatJalan`);
const data = await response.json();
showResponse(data, response.ok ? 'ok' : 'error');
} catch (error) {
showResponse({ error: error.message }, 'error');
}
}
async function getTarifBPJSRawatJalanByKode() {
const kode = document.getElementById('kodeRawatJalan').value;
if (!kode) {
alert('Silakan masukkan kode!');
return;
}
try {
showResponse('Loading...', 'loading');
const response = await fetch(`${API_BASE}/tarifBPJSRawatJalan/${encodeURIComponent(kode)}`);
const data = await response.json();
showResponse(data, response.ok ? 'ok' : 'error');
} catch (error) {
showResponse({ error: error.message }, 'error');
}
}
async function getTarifRS() {
try {
showResponse('Loading...', 'loading');
const response = await fetch(`${API_BASE}/tarifRS`);
const data = await response.json();
showResponse(data, response.ok ? 'ok' : 'error');
} catch (error) {
showResponse({ error: error.message }, 'error');
}
}
async function getTarifRSByKode() {
const kode = document.getElementById('kodeRS').value;
if (!kode) {
alert('Silakan masukkan kode!');
return;
}
try {
showResponse('Loading...', 'loading');
const response = await fetch(`${API_BASE}/tarifRS/${encodeURIComponent(kode)}`);
const data = await response.json();
showResponse(data, response.ok ? 'ok' : 'error');
} catch (error) {
showResponse({ error: error.message }, 'error');
}
}
async function getTarifRSByKategori() {
const kategori = document.getElementById('kategoriRS').value;
if (!kategori) {
alert('Silakan masukkan kategori!');
return;
}
try {
showResponse('Loading...', 'loading');
const response = await fetch(`${API_BASE}/tarifRSByKategori/${encodeURIComponent(kategori)}`);
const data = await response.json();
showResponse(data, response.ok ? 'ok' : 'error');
} catch (error) {
showResponse({ error: error.message }, 'error');
}
}
async function getICD9() {
try {
showResponse('Loading...', 'loading');
const response = await fetch(`${API_BASE}/icd9`);
const data = await response.json();
showResponse(data, response.ok ? 'ok' : 'error');
} catch (error) {
showResponse({ error: error.message }, 'error');
}
}
function filterICD9ClientSide() {
const input = document.getElementById('filterICD9');
const filter = input.value.toLowerCase();
const tableDiv = document.getElementById('tableResponse');
const table = tableDiv.querySelector('table');
if (!table) {
return;
}
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(filter) ? '' : 'none';
});
}
async function getICD10() {
try {
showResponse('Loading...', 'loading');
const response = await fetch(`${API_BASE}/icd10`);
const data = await response.json();
showResponse(data, response.ok ? 'ok' : 'error');
} catch (error) {
showResponse({ error: error.message }, 'error');
}
}
function filterICD10ClientSide() {
const input = document.getElementById('filterICD10');
const filter = input.value.toLowerCase();
const tableDiv = document.getElementById('tableResponse');
const table = tableDiv.querySelector('table');
if (!table) {
return;
}
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(filter) ? '' : 'none';
});
}
async function getDokter() {
try {
showResponse('Loading...', 'loading');
const response = await fetch(`${API_BASE}/dokter`);
const data = await response.json();
showResponse(data, response.ok ? 'ok' : 'error');
} catch (error) {
showResponse({ error: error.message }, 'error');
}
}
function filterDokterClientSide() {
const input = document.getElementById('filterDokter');
const filter = input.value.toLowerCase();
const tableDiv = document.getElementById('tableResponse');
const table = tableDiv.querySelector('table');
if (!table) {
return;
}
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(filter) ? '' : 'none';
});
}
async function getRuangan() {
try {
showResponse('Loading...', 'loading');
const response = await fetch(`${API_BASE}/ruangan`);
const data = await response.json();
showResponse(data, response.ok ? 'ok' : 'error');
} catch (error) {
showResponse({ error: error.message }, 'error');
}
}
function filterRuanganClientSide() {
const input = document.getElementById('filterRuangan');
const filter = input.value.toLowerCase();
const tableDiv = document.getElementById('tableResponse');
const table = tableDiv.querySelector('table');
if (!table) {
return;
}
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(filter) ? '' : 'none';
});
}
// Auto check health on load
window.onload = function() {
checkHealth();
};
</script>
</body>
</html>

45
frontendcareit_v4/.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# electron-builder
/dist/
/release/

View File

@@ -0,0 +1,191 @@
# Backend API Documentation
## Server Configuration
- **IP Address**: `10.10.11.34`
- **Port**: `8081`
- **Base URL**: `http://10.10.11.34:8081`
## Required Endpoints
### 1. Health Check
```
GET /tarifRS
```
**Response:**
```json
{
"status": "ok",
"timestamp": "2025-11-29T10:30:00Z"
}
```
### 2. Get Tarif Rumah Sakit
```
GET /tarifRS
```
**Query Parameters:**
- `kategori` (optional): Filter by category
- Values: `"Operatif"`, `"Non Operatif"`, `"Transplantasi Ginjal"`, `"Komplementer"`, `"Med Check Up"`
- `search` (optional): Search by kode or tindakan
- `page` (optional): Page number for pagination
- `limit` (optional): Items per page
**Example Request:**
```
GET /tarifRS?kategori=Operatif&search=ABDOMEN
```
**Response:**
```json
[
{
"id": 1,
"kode": "R.TO.0001",
"tindakan": "ABDOMEN, LAPAROTOMY, TRAUMA REOPERATION",
"tarif": "17.958.000",
"kategori": "Operatif",
"created_at": "2025-11-29T10:00:00Z",
"updated_at": "2025-11-29T10:00:00Z"
}
]
```
### 3. Create Tarif Rumah Sakit
```
POST /tarifRS
```
**Request Body:**
```json
{
"kode": "R.TO.0013",
"tindakan": "NEW PROCEDURE NAME",
"tarif": "5.000.000",
"kategori": "Operatif"
}
```
**Response:**
```json
{
"id": 13,
"kode": "R.TO.0013",
"tindakan": "NEW PROCEDURE NAME",
"tarif": "5.000.000",
"kategori": "Operatif",
"created_at": "2025-11-29T10:30:00Z",
"updated_at": "2025-11-29T10:30:00Z"
}
```
### 4. Update Tarif Rumah Sakit
```
PUT /tarifRS/{id}
```
**Request Body:** (partial update allowed)
```json
{
"tarif": "6.000.000"
}
```
### 5. Delete Tarif Rumah Sakit
```
DELETE /tarifRS/{id}
```
## Error Responses
### 400 Bad Request
```json
{
"error": "Bad Request",
"message": "Validation error details"
}
```
### 404 Not Found
```json
{
"error": "Not Found",
"message": "Resource not found"
}
```
### 500 Internal Server Error
```json
{
"error": "Internal Server Error",
"message": "Error description"
}
```
## CORS Configuration
Please ensure your backend allows CORS for:
- **Origin**: `http://localhost:3000` (development)
- **Methods**: `GET, POST, PUT, DELETE, OPTIONS`
- **Headers**: `Content-Type, Accept, Authorization`
## Database Schema (Suggested)
### Table: `tarif_rumah_sakit`
```sql
CREATE TABLE tarif_rumah_sakit (
id SERIAL PRIMARY KEY,
kode VARCHAR(50) UNIQUE NOT NULL,
tindakan TEXT NOT NULL,
tarif VARCHAR(50) NOT NULL,
kategori VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```
### Sample Data
```sql
INSERT INTO tarif_rumah_sakit (kode, tindakan, tarif, kategori) VALUES
('R.TO.0001', 'ABDOMEN, LAPAROTOMY, TRAUMA REOPERATION', '17.958.000', 'Operatif'),
('R.TO.0002', 'APPENDECTOMY, LAPAROSCOPIC', '12.500.000', 'Operatif'),
('R.NO.0001', 'KONSULTASI SPESIALIS BEDAH', '350.000', 'Non Operatif'),
('R.TG.0001', 'TRANSPLANTASI GINJAL DONOR HIDUP', '125.000.000', 'Transplantasi Ginjal'),
('R.KM.0001', 'AKUPUNKTUR MEDIK', '200.000', 'Komplementer'),
('R.MC.0001', 'MEDICAL CHECK UP BASIC', '750.000', 'Med Check Up');
```
## Testing
You can test the API endpoints using:
- **Postman**: Import the endpoints above
- **curl**: `curl -X GET http://10.10.11.34:8081/tarifRS`
- **Browser**: Navigate to `http://10.10.11.34:8081/tarifRS`
## Notes
- All timestamps should be in ISO 8601 format
- Tarif values are stored as strings to preserve formatting
- Category values must match exactly (case-sensitive)
- Search should be case-insensitive for kode and tindakan fields

View File

@@ -0,0 +1,87 @@
# Konfigurasi API untuk Electron App
## Cara Kerja API di Electron
Di Electron app, API calls **langsung ke backend Go** (tidak melalui Next.js API routes), karena:
- Electron menggunakan static files (tidak ada Next.js server)
- API routes Next.js tidak bisa berjalan di static export
- Solusi: langsung call backend Go
## Konfigurasi API URL
Ada 3 cara untuk set API URL:
### 1. Menggunakan `electron.config.js` (Recommended)
Edit file `electron.config.js`:
```javascript
module.exports = {
apiUrl: 'http://31.97.109.192:8081', // Ganti dengan URL backend Anda
};
```
### 2. Menggunakan Environment Variable
Set sebelum build atau run:
```powershell
# PowerShell
$env:NEXT_PUBLIC_API_URL = "http://31.97.109.192:8081"
npm run electron:dev
# Atau saat build
$env:NEXT_PUBLIC_API_URL = "http://31.97.109.192:8081"
.\build-electron.ps1
```
### 3. Default (localhost)
Jika tidak di-set, akan menggunakan: `http://localhost:8081`
## Testing API Connection
1. Pastikan backend Go berjalan
2. Test dengan Electron dev mode:
```bash
npm run dev # Terminal 1: Next.js dev server
npm run electron:dev # Terminal 2: Electron app
```
3. Buka DevTools di Electron (F12) dan cek console untuk melihat API URL yang digunakan
## Build untuk Production
Saat build installer, API URL akan di-inject ke aplikasi:
```powershell
# Set API URL untuk production
$env:NEXT_PUBLIC_API_URL = "http://31.97.109.192:8081"
.\build-electron.ps1
```
Atau edit `electron.config.js` sebelum build.
## CORS Configuration
Pastikan backend Go mengizinkan request dari Electron app. Di backend Go, pastikan CORS config mengizinkan:
- Origin: `file://` (untuk Electron)
- Atau semua origin untuk development
## Troubleshooting
### API tidak connect
1. Cek console di Electron (F12) untuk melihat error
2. Pastikan backend Go berjalan dan bisa diakses
3. Cek API URL yang digunakan (akan di-log di console)
4. Pastikan CORS di backend sudah benar
### API URL tidak ter-update
1. Restart Electron app setelah mengubah config
2. Rebuild aplikasi jika sudah di-build
## Catatan Penting
- API URL di-inject saat runtime, bukan saat build
- Setiap user bisa mengubah `electron.config.js` untuk mengubah API URL
- Untuk production, pertimbangkan hardcode API URL atau gunakan config file yang bisa diubah user

View File

@@ -0,0 +1,82 @@
# PANDUAN ELECTRON - CareIt Desktop App
## Persiapan Backend API
1. **Konfigurasi URL Backend**
Buat file `.env.local` di root folder project (jika belum ada):
```bash
NEXT_PUBLIC_API_URL=http://localhost:8081
```
Ganti `http://localhost:8081` dengan URL backend Golang yang sudah di-deploy.
Contoh:
- Development: `http://localhost:8081`
- Production: `https://api-careit.example.com`
## Cara Menjalankan
### 1. Install Dependencies
```bash
npm install
```
### 2. Development Mode (untuk testing)
```bash
# Terminal 1: Jalankan Next.js dev server
npm run dev
# Terminal 2: Jalankan Electron
npm run electron:dev
```
### 3. Build dan Package untuk Windows EXE
#### Build Aplikasi
```bash
npm run electron:build
```
File `.exe` akan ada di folder `dist/` setelah build selesai.
#### Untuk 32-bit dan 64-bit
```bash
npm run electron:build:all
```
## Struktur File Electron
- `electron.js` - Main process Electron
- `preload.js` - Bridge script untuk security
- `out/` - Next.js static export output
- `dist/` - Folder hasil build Electron (berisi installer .exe)
## Troubleshooting
### Error: "Cannot find module electron"
```bash
npm install
```
### Error: Backend tidak terhubung
- Pastikan file `.env.local` sudah dibuat
- Cek URL backend di `.env.local` sudah benar
- Pastikan backend Golang sudah running
### Icon tidak muncul
- Pastikan ada file `icon.png` di folder `public/`
- Ukuran minimal 256x256 pixels
- Format: PNG dengan transparency
## Distribusi
Setelah build, file installer ada di:
- `dist/CareIt Setup 0.1.0.exe` - Installer untuk Windows
File ini bisa langsung dibagikan ke user lain tanpa perlu install Node.js/npm.
## Catatan Penting
- Aplikasi Electron akan langsung connect ke backend Golang (tidak pakai Next.js API routes)
- Pastikan backend sudah running dan URL di `.env.local` benar
- CORS harus dikonfigurasi di backend agar menerima request dari Electron

View File

@@ -0,0 +1,138 @@
# Panduan Integrasi Frontend dengan Backend
Dokumen ini menjelaskan bagaimana frontend Next.js terintegrasi dengan backend Go.
## Konfigurasi
### Backend URL
Backend berjalan di `http://localhost:8081` secara default. Untuk mengubah URL backend:
1. Buat file `.env.local` di folder `Frontend_CareIt/` (jika belum ada)
2. Tambahkan:
```
NEXT_PUBLIC_API_URL=http://localhost:8081
```
Atau ganti dengan IP address backend Anda jika berbeda.
### CORS
Backend sudah dikonfigurasi dengan CORS default yang mengizinkan semua origin. Untuk production, sebaiknya dikonfigurasi lebih spesifik.
## API Functions
Semua fungsi API tersedia di `src/lib/api.ts`. Fungsi-fungsi utama:
### Authentication
- `loginDokter(credentials)` - Login dokter dengan email dan password
### Data Master
- `getDokter()` - Ambil daftar dokter
- `getRuangan()` - Ambil daftar ruangan
- `getICD9()` - Ambil daftar ICD9
- `getICD10()` - Ambil daftar ICD10
- `getTarifRumahSakit(params)` - Ambil tarif rumah sakit
- `getTarifBPJSRawatInap()` - Ambil tarif BPJS rawat inap
- `getTarifBPJSRawatJalan()` - Ambil tarif BPJS rawat jalan
### Pasien
- `getPasienById(id)` - Ambil data pasien by ID
- `searchPasien(nama)` - Cari pasien by nama
### Billing
- `createBilling(data)` - Buat billing baru
- `getBillingAktifByNama(namaPasien)` - Ambil billing aktif by nama pasien
- `getAllBilling()` - Ambil semua billing (untuk admin)
### Admin
- `postINACBGAdmin(data)` - Post INACBG dari admin
## Komponen yang Sudah Terintegrasi
### 1. Login (`login.tsx`)
- Menggunakan API `loginDokter` untuk autentikasi
- Menyimpan token dan data dokter di localStorage
- Menampilkan error jika login gagal
### 2. Billing Pasien (`billing-pasien.tsx`)
- Fetch data dropdown (dokter, ruangan, ICD9, ICD10, tarif RS) dari backend
- Search pasien menggunakan API
- Create billing dengan API `createBilling`
- Auto-calculate total tarif RS berdasarkan tindakan yang dipilih
### 3. Riwayat Billing Pasien (`riwayat-billing-pasien.tsx`)
- Fetch semua billing dari API `getAllBilling`
- Search/filter billing berdasarkan nama atau ID
- Menampilkan status billing dengan color coding
## Cara Menggunakan
### 1. Menjalankan Backend
```bash
cd Backend_CareIt
go run main.go
```
Backend akan berjalan di `http://localhost:8081`
### 2. Menjalankan Frontend
```bash
cd Frontend_CareIt
npm run dev
```
Frontend akan berjalan di `http://localhost:3000`
### 3. Testing
1. Buka browser ke `http://localhost:3000`
2. Login menggunakan email dan password dokter yang ada di database
3. Test fitur billing pasien
4. Test dashboard admin untuk melihat riwayat billing
## Troubleshooting
### Error: "Tidak dapat terhubung ke server"
- Pastikan backend server berjalan di port 8081
- Cek apakah URL di `.env.local` sudah benar
- Cek firewall atau network settings
### Error: "CORS error"
- Backend sudah dikonfigurasi dengan CORS default
- Jika masih error, pastikan backend menggunakan `cors.Default()` atau konfigurasi CORS yang sesuai
### Error: "Data tidak ditemukan"
- Pastikan database sudah terisi dengan data
- Cek endpoint API di backend apakah sudah benar
## Catatan Penting
1. **Token Management**: Token login disimpan di localStorage. Untuk production, pertimbangkan menggunakan httpOnly cookies atau session management yang lebih aman.
2. **Error Handling**: Semua API calls sudah memiliki error handling. Error akan ditampilkan di UI.
3. **Loading States**: Komponen yang fetch data akan menampilkan loading state saat mengambil data.
4. **Data Validation**: Form validation dilakukan di frontend sebelum submit ke backend.
## Endpoint Backend yang Tersedia
- `GET /` - Health check
- `GET /dokter` - List dokter
- `GET /ruangan` - List ruangan
- `GET /icd9` - List ICD9
- `GET /icd10` - List ICD10
- `GET /tarifRS` - List tarif rumah sakit
- `GET /tarifBPJSRawatInap` - List tarif BPJS rawat inap
- `GET /tarifBPJSRawatJalan` - List tarif BPJS rawat jalan
- `GET /pasien/:id` - Get pasien by ID
- `GET /pasien/search?nama=...` - Search pasien
- `POST /login` - Login dokter
- `POST /billing` - Create billing
- `GET /billing/aktif?nama_pasien=...` - Get billing aktif
- `GET /admin/billing` - Get all billing (admin)
- `POST /admin/inacbg` - Post INACBG (admin)
## Next Steps
1. Update komponen tarif-bpjs untuk menggunakan API real
2. Implementasi authentication middleware untuk protected routes
3. Add refresh token mechanism
4. Improve error handling dan user feedback
5. Add loading skeletons untuk better UX

104
frontendcareit_v4/README.md Normal file
View File

@@ -0,0 +1,104 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app) and configured for mobile deployment with [Capacitor](https://capacitorjs.com/).
## Getting Started
### Web Development
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
### Mobile Development (Capacitor)
This project supports both web and mobile platforms:
- **Web**: Uses Next.js API routes as proxy to backend (no CORS issues)
- **Mobile**: Uses direct backend calls (static export, no API routes)
**Prerequisites:**
- Backend server must be running (default: `http://localhost:8081`)
- For production mobile app, set `NEXT_PUBLIC_API_URL` environment variable to your backend server URL
**Build for Mobile (with static export):**
**Windows:**
```powershell
# PowerShell
.\build-mobile.ps1
# Or CMD
build-mobile.bat
# Or manually set environment variable
$env:NEXT_EXPORT="true"; npm run build; npx cap sync
```
**Linux/Mac:**
```bash
# Set environment variable and build
NEXT_EXPORT=true npm run build && npx cap sync
# Or use the npm script (requires cross-env)
npm run build:mobile:export
```
**Note:** Regular `npm run build` is for web development (with API routes). Use the mobile build scripts above for Capacitor.
**Open Native Projects:**
```bash
# Android
npm run cap:open:android
# iOS (macOS only)
npm run cap:open:ios
```
**Run on Device/Emulator:**
```bash
# Android
npm run cap:run:android
# iOS
npm run cap:run:ios
```
**Other Capacitor Commands:**
```bash
# Copy web assets only (without updating dependencies)
npm run cap:copy
# Sync web assets and update native dependencies
npm run cap:sync
```
**How It Works:**
- **Web Development**: API calls go through `/api` routes (Next.js proxy) → Backend Go server
- **Mobile App**: API calls go directly to backend Go server (detected automatically via Capacitor)
- The app automatically detects the platform and uses the appropriate API endpoint
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@@ -0,0 +1,205 @@
# Tanggal Keluar Auto-Fill Flow Documentation
## Overview
Ketika admin/dokter memasukkan kode INACBG, `tanggal_keluar` **TIDAK** otomatis terisi dari field form, tetapi **OTOMATIS DIKIRIM** ke backend menggunakan nilai fallback.
---
## Alur Lengkap
### 1. **INACBG_Admin_Ruangan.tsx** - Ketika Submit (handleSave)
**Location:** [Line 705-715](../frontendcareitv2/src/app/component/INACBG_Admin_Ruangan.tsx#L705-L715)
```typescript
const payload: PostINACBGRequest = {
id_billing: billingId,
tipe_inacbg: tipeInacbg,
kode_inacbg: selectedInacbgCodes,
total_klaim: deltaKlaim,
billing_sign: billingSignColor,
tanggal_keluar: tanggalKeluar || new Date().toISOString().split("T")[0],
};
```
**Logika:**
- `tanggalKeluar` sudah di-load dari API saat page load (dari `billingId`)
- Jika `tanggalKeluar` kosong/undefined, gunakan **hari ini** (`new Date().toISOString().split("T")[0]`)
- Kirim ke `/admin/inacbg` endpoint dengan nilai tanggal (entah dari user edit atau default hari ini)
**Flow yang terjadi:**
1. User memilih kode INACBG → `setSelectedInacbgCodes([...codes])`
2. User klik tombol "Simpan" → `handleSave()` dipanggil
3. Di dalam `handleSave()`:
- Cek `selectedInacbgCodes.length > 0` (minimal 1 kode harus dipilih)
- Buat payload dengan `tanggal_keluar: tanggalKeluar || new Date().toISOString().split("T")[0]`
- POST ke `/admin/inacbg`
---
### 2. **billing-pasien.tsx** - Ketika Create Billing (handleSubmit)
**Location:** [Line 650-655](../frontendcareitv2/src/app/component/billing-pasien.tsx#L650-L655)
```typescript
const billingData: BillingRequest = {
nama_pasien: namaPasien,
// ... other fields ...
tanggal_masuk: convertDateFormat(tanggalMasuk),
tanggal_keluar: convertDateFormat(tanggalKeluar) || '',
// ... other fields ...
};
```
**Logika:**
- User opsional mengisi `tanggal_keluar` di billing form
- Jika kosong, kirim string kosong `''`
- Backend akan handle nullable field
**Flow yang terjadi:**
1. User fill form + select tindakan/ICD10
2. User klik "Buat Billing" → `handleSubmit()`
3. Format tanggal dengan `convertDateFormat()` (handle both YYYY-MM-DD dan DD/MM/YYYY)
4. POST ke `/billing` endpoint
---
### 3. **Data Loading - Tanggal Keluar di-Fetch dari API**
**INACBG_Admin_Ruangan.tsx** - [Line 106-245](../frontendcareitv2/src/app/component/INACBG_Admin_Ruangan.tsx#L106-L245)
Ada 3 priority untuk load tanggal_keluar:
```typescript
// PRIORITY 1: From props (jika parent pass data)
if (pasienData) {
// Fetch dari API /admin/billing/{billingId}
const response = await apiFetch<any>(`/admin/billing/${billingId}`);
const tanggalKeluarData = response.data.tanggal_keluar || response.data.Tanggal_keluar || "";
setTanggalKeluar(tanggalKeluarData);
}
// PRIORITY 2: From localStorage (jika data sudah disimpan sebelumnya)
const storedData = localStorage.getItem('currentBillingData');
if (storedData) {
// Fetch dari API /admin/billing/{billingId}
const response = await apiFetch<any>(`/admin/billing/${billingId}`);
const tanggalKeluarData = response.data.tanggal_keluar || response.data.Tanggal_keluar || "";
setTanggalKeluar(tanggalKeluarData);
}
// PRIORITY 3: From API direct fetch (jika billingId tersedia)
if (billingId) {
const response = await apiFetch<any>(`/admin/billing/${billingId}`);
const tanggalKeluarData = data.tanggal_keluar || data.Tanggal_keluar || "";
setTanggalKeluar(tanggalKeluarData);
}
```
---
## Form Input untuk Tanggal Keluar
### INACBG_Admin_Ruangan.tsx
**Location:** [Line 1004-1012](../frontendcareitv2/src/app/component/INACBG_Admin_Ruangan.tsx#L1004-L1012)
```typescript
<div className="ml-0 sm:ml-4 mt-2">
<label className="block text-sm sm:text-md text-[#2591D0] mb-1 sm:mb-2 font-bold">
Tanggal Keluar (Opsional)
</label>
<input
type="date"
value={tanggalKeluar}
onChange={(e) => setTanggalKeluar(e.target.value)}
className="w-full border text-sm border-blue-200 rounded-full py-2 sm:py-3 pl-3 sm:pl-4 pr-8 sm:pr-10 text-[#2591D0] bg-white focus:ring-2 focus:ring-blue-400 focus:border-blue-400 focus:outline-0"
/>
</div>
```
**Status:**
- ✅ Input field ada
- ✅ Auto-filled dari API saat page load
- ✅ User bisa edit nilai
- ✅ Kirim ke backend saat submit
---
## Riwayat Billing Aktif - Kolom Tanggal Ditampilkan
### billing-pasien.tsx
**Location:** [Line 1215-1238](../frontendcareitv2/src/app/component/billing-pasien.tsx#L1215-L1238) (Desktop Table)
```typescript
<table className="w-full text-sm md:text-base border-collapse">
<thead>
<tr className="bg-blue-100 border-b border-blue-200">
<th>Tanggal Masuk</th>
<th>Tanggal Keluar</th>
<th>Tindakan RS</th>
<th>ICD 9</th>
<th>ICD 10</th>
<th>INACBG</th>
</tr>
</thead>
<tbody>
{/* Loop untuk show dates dan data */}
<td>
{billingHistory.tanggal_masuk
? new Date(billingHistory.tanggal_masuk).toLocaleDateString('id-ID', {...})
: '-'}
</td>
<td>
{billingHistory.tanggal_keluar
? new Date(billingHistory.tanggal_keluar).toLocaleDateString('id-ID', {...})
: '-'}
</td>
</tbody>
</table>
```
### INACBG_Admin_Ruangan.tsx
**Location:** [Line 1047-1070](../frontendcareitv2/src/app/component/INACBG_Admin_Ruangan.tsx#L1047-L1070) (Desktop Table)
```typescript
<table className="w-full text-sm md:text-base border-collapse">
<thead>
<tr className="bg-blue-100 border-b border-blue-200">
<th>Tanggal Masuk</th>
<th>Tanggal Keluar</th>
<th>ICD 9</th>
<th>ICD 10</th>
<th>INACBG</th>
</tr>
</thead>
</table>
```
---
## Debugging Logs
Cek browser console (F12) untuk melihat flow:
**INACBG_Admin_Ruangan.tsx:**
```
📥 Tanggal keluar from API: [nilai tanggal atau empty]
📅 Extracted dates from billing history: { tanggalMasuk: ..., tanggalKeluar: ... }
📤 Sending INACBG payload: { tanggal_keluar: ..., ... }
```
**billing-pasien.tsx:**
```
💾 Extracted dates: { tanggalMasuk, tanggalKeluar, billingObj }
💾 Patient data saved to localStorage: { tanggal_masuk, tanggal_keluar }
```
---
## Summary
| Aspek | INACBG Form | Billing Pasien Form |
|-------|-------------|-------------------|
| **Input Field** | ✅ Ada (type=date) | ✅ Ada (type=date) |
| **Auto-fill saat load** | ✅ Dari API | ✅ Dari API |
| **User bisa edit** | ✅ Ya | ✅ Ya |
| **Submit behavior** | Gunakan nilai yang ada atau hari ini | Kirim nilai atau kosong |
| **Riwayat ditampilkan** | ✅ Ya, di table | ✅ Ya, di table |
| **Format tanggal** | ISO format dari backend | YYYY-MM-DD atau DD/MM/YYYY |

101
frontendcareit_v4/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

View File

@@ -0,0 +1,4 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK" />
</project>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep

View File

@@ -0,0 +1,54 @@
apply plugin: 'com.android.application'
android {
namespace = "com.CareIt.app"
compileSdk = rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.CareIt.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

View File

@@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,26 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@@ -0,0 +1,5 @@
package com.CareIt.app;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Some files were not shown because too many files have changed in this diff Show More