first commit
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Billing INACBG</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row h-100">
|
||||
<!-- Sidebar -->
|
||||
<div class="col-md-3 sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h5>Ruangan</h5>
|
||||
</div>
|
||||
<div class="ruangan-list" id="ruanganList">
|
||||
<!-- Will be populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="col-md-9 main-content">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h2>Data Billing Pasien</h2>
|
||||
<div class="text-muted small" id="currentDate"></div>
|
||||
</div>
|
||||
<div class="search-box mt-3">
|
||||
<input type="text" id="searchInput" class="form-control" placeholder="Cari billing pasien dian">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Billing Table -->
|
||||
<div class="billing-table-container">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID Pasien</th>
|
||||
<th>Nama</th>
|
||||
<th>Total Tarif RS</th>
|
||||
<th>Total Klaim BPJS</th>
|
||||
<th>Billing Sign</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="billingTableBody">
|
||||
<!-- Will be populated by JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editModalLabel">Data Pasien</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Patient Info Section -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary">Nama Lengkap</h6>
|
||||
<input type="text" id="modalNamaPasien" class="form-control" readonly>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-secondary">ID Pasien</h6>
|
||||
<input type="text" id="modalIdPasien" class="form-control" readonly>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-secondary">Kelas</h6>
|
||||
<input type="text" id="modalKelas" class="form-control" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dokter yang Menangani -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary fw-bold">Dokter yang Menangani</h6>
|
||||
<div id="dokterList" class="border rounded p-2 bg-light small">
|
||||
<span class="text-muted">Memuat data dokter...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tindakan & ICD - Pisah Lama vs Baru -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary fw-bold">Tindakan RS</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">Data yang sudah ada:</small>
|
||||
<div id="tindakanLama" class="border rounded p-2 bg-light small"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">Data baru (akan ditambahkan):</small>
|
||||
<div id="tindakanBaru" class="border rounded p-2 bg-light small text-muted">Belum ada data baru</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary fw-bold">ICD 9 & ICD 10</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">ICD 9 - Data yang sudah ada:</small>
|
||||
<div id="icd9Lama" class="border rounded p-2 bg-light small mb-2"></div>
|
||||
<small class="text-muted">ICD 10 - Data yang sudah ada:</small>
|
||||
<div id="icd10Lama" class="border rounded p-2 bg-light small"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">ICD 9 - Data baru:</small>
|
||||
<div id="icd9Baru" class="border rounded p-2 bg-light small mb-2 text-muted">Belum ada data baru</div>
|
||||
<small class="text-muted">ICD 10 - Data baru:</small>
|
||||
<div id="icd10Baru" class="border rounded p-2 bg-light small text-muted">Belum ada data baru</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary">Total Tarif RS (Kumulatif)</h6>
|
||||
<input type="text" id="modalTotalTarif" class="form-control" readonly>
|
||||
</div>
|
||||
|
||||
<!-- INACBG Form -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary">INA CBG</h6>
|
||||
<form id="inacbgForm">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Tipe INACBG</label>
|
||||
<select id="tipeInacbg" class="form-select">
|
||||
<option value="">-- Pilih Tipe --</option>
|
||||
<option value="RI">RI (Rawat Inap)</option>
|
||||
<option value="RJ">RJ (Rawat Jalan)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Masukkan Kode INA CBGS</label>
|
||||
<div class="input-group">
|
||||
<select id="inacbgCode" class="form-select" disabled>
|
||||
<option value="">-- Pilih Tipe INACBG Dulu --</option>
|
||||
</select>
|
||||
<span class="input-group-text" style="cursor: pointer; user-select: none;" title="Ganti ke input manual" onclick="toggleInacbgInput()">
|
||||
↔️
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" id="inacbgCodeManual" class="form-control d-none mt-2" placeholder="Ketik kode INACBG manual">
|
||||
</div>
|
||||
<div class="col-md-6 d-flex align-items-end">
|
||||
<button type="button" id="addCodeBtn" class="btn btn-primary w-100">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- INACBG Lama -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">INACBG yang sudah ada sebelumnya:</label>
|
||||
<div id="inacbgLamaContainer" class="border rounded p-2 bg-light small">
|
||||
<div id="inacbgRILama" class="mb-1"></div>
|
||||
<div id="inacbgRJLama"></div>
|
||||
<div id="totalKlaimLama" class="mt-2 fw-bold"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- INACBG Baru -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">INACBG Baru (akan ditambahkan):</label>
|
||||
<div id="codeList" class="border rounded p-2 bg-light">
|
||||
<small class="text-muted">Belum ada kode baru</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Total Klaim Lama</label>
|
||||
<input type="number" id="totalKlaimLamaInput" class="form-control" placeholder="0" readonly>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Total Klaim Baru <span class="text-muted small">(Otomatis)</span></label>
|
||||
<input type="number" id="totalKlaim" class="form-control" placeholder="0" readonly>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Total Klaim Akhir</label>
|
||||
<input type="number" id="totalKlaimAkhir" class="form-control fw-bold" placeholder="0" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Billing Sign</label>
|
||||
<div id="billingSignContainer" class="mt-1">
|
||||
<span id="billingSignBadge" class="badge bg-secondary">-</span>
|
||||
<span id="billingSignText" class="ms-2 text-muted small"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Tanggal Keluar</label>
|
||||
<input type="date" id="tanggalKeluar" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="formAlert" class="alert d-none" role="alert"></div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-success">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="scriptAdmin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,158 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Billing INACBG</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row h-100">
|
||||
<!-- Sidebar -->
|
||||
<div class="col-md-3 sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h5>Ruangan</h5>
|
||||
</div>
|
||||
<div class="ruangan-list" id="ruanganList">
|
||||
<!-- Will be populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="col-md-9 main-content">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h2>Data Billing Pasien</h2>
|
||||
<div class="text-muted small" id="currentDate"></div>
|
||||
</div>
|
||||
<div class="search-box mt-3">
|
||||
<input type="text" id="searchInput" class="form-control" placeholder="Cari billing pasien dian">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Billing Table -->
|
||||
<div class="billing-table-container">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID Pasien</th>
|
||||
<th>Nama</th>
|
||||
<th>Billing Sign</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="billingTableBody">
|
||||
<!-- Will be populated by JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editModalLabel">Data Pasien</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Patient Info Section -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary">Nama Lengkap</h6>
|
||||
<input type="text" id="modalNamaPasien" class="form-control" readonly>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-secondary">ID Pasien</h6>
|
||||
<input type="text" id="modalIdPasien" class="form-control" readonly>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-secondary">Kelas</h6>
|
||||
<input type="text" id="modalKelas" class="form-control" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tindakan & Pemeriksaan -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary">Tindakan dan Pemeriksaan Penunjang</h6>
|
||||
<input type="text" id="modalTindakan" class="form-control" readonly placeholder="(diambil dari billing)">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary">Total Tarif RS</h6>
|
||||
<input type="text" id="modalTotalTarif" class="form-control" readonly>
|
||||
</div>
|
||||
|
||||
<!-- ICD Codes -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-secondary">ICD 9</h6>
|
||||
<input type="text" id="modalICD9" class="form-control" readonly>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-secondary">ICD 10</h6>
|
||||
<input type="text" id="modalICD10" class="form-control" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- INACBG Form -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary">INA CBG</h6>
|
||||
<form id="inacbgForm">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Tipe INACBG</label>
|
||||
<select id="tipeInacbg" class="form-select">
|
||||
<option value="">-- Pilih Tipe --</option>
|
||||
<option value="RI">RI (Rawat Inap)</option>
|
||||
<option value="RJ">RJ (Rawat Jalan)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Masukkan Kode INA CBGS</label>
|
||||
<select id="inacbgCode" class="form-select" disabled>
|
||||
<option value="">-- Pilih Tipe INACBG Dulu --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 d-flex align-items-end">
|
||||
<button type="button" id="addCodeBtn" class="btn btn-primary">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="codeList" class="mb-3">
|
||||
<!-- Added codes will appear here -->
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Total Klaim BJPS <span class="text-muted small">(Otomatis)</span></label>
|
||||
<input type="number" id="totalKlaim" class="form-control" placeholder="0" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="formAlert" class="alert d-none" role="alert"></div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-success">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="scriptAdmin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,715 @@
|
||||
// Configuration
|
||||
const API_BASE = 'http://localhost:8081';
|
||||
let billingData = [];
|
||||
let currentEditingBilling = null;
|
||||
let inacbgCodes = [];
|
||||
let tarifCache = {}; // Cache for tarif data
|
||||
let isManualInacbgMode = false; // Track if user is in manual input mode
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateCurrentDate();
|
||||
loadBillingData();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
// Update current date
|
||||
function updateCurrentDate() {
|
||||
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
||||
const today = new Date().toLocaleDateString('id-ID', options);
|
||||
document.getElementById('currentDate').textContent = today;
|
||||
}
|
||||
|
||||
// Load billing data from API
|
||||
async function loadBillingData() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/admin/billing`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const data = await res.json();
|
||||
billingData = data.data || [];
|
||||
console.log('Billing data loaded:', billingData);
|
||||
// Debug: cek apakah total_klaim ada di response
|
||||
if (billingData.length > 0) {
|
||||
console.log('Sample billing item:', billingData[0]);
|
||||
console.log('Total klaim dari sample:', billingData[0].total_klaim);
|
||||
}
|
||||
|
||||
renderBillingTable();
|
||||
renderRuanganSidebar();
|
||||
} catch (err) {
|
||||
console.error('Error loading billing data:', err);
|
||||
document.getElementById('billingTableBody').innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-danger">Gagal memuat data: ${err.message}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Render billing table
|
||||
function renderBillingTable() {
|
||||
const tbody = document.getElementById('billingTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (billingData.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">Tidak ada data billing</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
billingData.forEach(billing => {
|
||||
const row = document.createElement('tr');
|
||||
const badgeClass = getBillingSignBadgeClass(billing.billing_sign);
|
||||
const badgeColor = getBillingSignColor(billing.billing_sign);
|
||||
|
||||
const totalTarif = billing.total_tarif_rs || 0;
|
||||
const totalKlaim = billing.total_klaim || 0;
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${billing.id_pasien || '-'}</td>
|
||||
<td>
|
||||
<a href="#" class="text-primary text-decoration-none" onclick="openEditModal(${billing.id_billing}); return false;">
|
||||
${billing.nama_pasien || '-'}
|
||||
</a>
|
||||
</td>
|
||||
<td>Rp ${Number(totalTarif).toLocaleString('id-ID')}</td>
|
||||
<td>Rp ${Number(totalKlaim).toLocaleString('id-ID')}</td>
|
||||
<td>
|
||||
<span class="billing-sign-badge ${badgeClass}" style="background-color: ${badgeColor};" title="${billing.billing_sign}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="openEditModal(${billing.id_billing})">
|
||||
✎ Edit
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Get billing sign badge class and color
|
||||
function getBillingSignColor(billingSign) {
|
||||
const normalizedSign = (billingSign || '').toString().toLowerCase();
|
||||
switch (normalizedSign) {
|
||||
case 'hijau':
|
||||
return '#28a745';
|
||||
case 'kuning':
|
||||
return '#ffc107';
|
||||
case 'orange':
|
||||
return '#fd7e14';
|
||||
case 'merah':
|
||||
case 'created':
|
||||
return '#dc3545';
|
||||
default:
|
||||
return '#6c757d';
|
||||
}
|
||||
}
|
||||
|
||||
function getBillingSignBadgeClass(billingSign) {
|
||||
const normalizedSign = (billingSign || '').toString().toLowerCase();
|
||||
switch (normalizedSign) {
|
||||
case 'hijau':
|
||||
return 'hijau';
|
||||
case 'kuning':
|
||||
return 'kuning';
|
||||
case 'orange':
|
||||
return 'orange';
|
||||
case 'merah':
|
||||
return 'merah';
|
||||
case 'created':
|
||||
return 'created';
|
||||
default:
|
||||
return 'created';
|
||||
}
|
||||
}
|
||||
|
||||
// Render ruangan sidebar
|
||||
function renderRuanganSidebar() {
|
||||
const uniqueRuangans = [...new Set(billingData.map(b => b.ruangan))];
|
||||
const ruanganList = document.getElementById('ruanganList');
|
||||
ruanganList.innerHTML = '';
|
||||
|
||||
if (uniqueRuangans.length === 0) {
|
||||
ruanganList.innerHTML = '<p class="text-muted">Tidak ada ruangan</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
uniqueRuangans.forEach((ruangan, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'ruangan-item';
|
||||
item.textContent = ruangan || `Ruangan ${index + 1}`;
|
||||
item.onclick = () => filterByRuangan(ruangan);
|
||||
ruanganList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter billing by ruangan
|
||||
function filterByRuangan(ruangan) {
|
||||
const filtered = billingData.filter(b => b.ruangan === ruangan);
|
||||
const tbody = document.getElementById('billingTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">Tidak ada data untuk ruangan ini</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
filtered.forEach(billing => {
|
||||
const row = document.createElement('tr');
|
||||
const badgeColor = getBillingSignColor(billing.billing_sign);
|
||||
const badgeClass = getBillingSignBadgeClass(billing.billing_sign);
|
||||
|
||||
const totalTarif = billing.total_tarif_rs || 0;
|
||||
const totalKlaim = billing.total_klaim || 0;
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${billing.id_pasien || '-'}</td>
|
||||
<td>
|
||||
<a href="#" class="text-primary text-decoration-none" onclick="openEditModal(${billing.id_billing}); return false;">
|
||||
${billing.nama_pasien || '-'}
|
||||
</a>
|
||||
</td>
|
||||
<td>Rp ${Number(totalTarif).toLocaleString('id-ID')}</td>
|
||||
<td>Rp ${Number(totalKlaim).toLocaleString('id-ID')}</td>
|
||||
<td>
|
||||
<span class="billing-sign-badge ${badgeClass}" style="background-color: ${badgeColor};" title="${billing.billing_sign}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="openEditModal(${billing.id_billing})">
|
||||
✎ Edit
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Open edit modal
|
||||
function openEditModal(billingId) {
|
||||
currentEditingBilling = billingData.find(b => b.id_billing === billingId);
|
||||
if (!currentEditingBilling) {
|
||||
alert('Data billing tidak ditemukan');
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate modal with billing data
|
||||
document.getElementById('modalNamaPasien').value = currentEditingBilling.nama_pasien || '';
|
||||
document.getElementById('modalIdPasien').value = currentEditingBilling.id_pasien || '';
|
||||
document.getElementById('modalKelas').value = currentEditingBilling.Kelas || '';
|
||||
|
||||
// Tampilkan dokter yang menangani pasien
|
||||
const dokterList = currentEditingBilling.nama_dokter || [];
|
||||
const dokterListEl = document.getElementById('dokterList');
|
||||
if (dokterList.length > 0) {
|
||||
dokterListEl.innerHTML = dokterList.map(dokter =>
|
||||
`<span class="badge bg-info me-2 mb-1">${dokter}</span>`
|
||||
).join('');
|
||||
} else {
|
||||
dokterListEl.innerHTML = '<span class="text-muted">Belum ada data dokter</span>';
|
||||
}
|
||||
|
||||
// Total tarif & total klaim kumulatif
|
||||
// Handle berbagai kemungkinan nama field (case-insensitive)
|
||||
const totalTarif = Number(currentEditingBilling.total_tarif_rs || currentEditingBilling.Total_Tarif_RS || 0);
|
||||
const totalKlaimLama = Number(currentEditingBilling.total_klaim || currentEditingBilling.Total_Klaim || currentEditingBilling.total_klaim_lama || 0);
|
||||
document.getElementById('modalTotalTarif').value = totalTarif.toLocaleString('id-ID');
|
||||
|
||||
// Tindakan RS - semua yang ada sekarang = "lama" (karena tidak ada cara membedakan mana yang baru)
|
||||
const tindakanLama = currentEditingBilling.tindakan_rs || [];
|
||||
document.getElementById('tindakanLama').textContent = tindakanLama.length > 0 ? tindakanLama.join(', ') : 'Tidak ada';
|
||||
document.getElementById('tindakanBaru').textContent = 'Belum ada data baru';
|
||||
|
||||
// ICD9 & ICD10 - semua yang ada sekarang = "lama"
|
||||
const icd9Lama = currentEditingBilling.icd9 || [];
|
||||
const icd10Lama = currentEditingBilling.icd10 || [];
|
||||
document.getElementById('icd9Lama').textContent = icd9Lama.length > 0 ? icd9Lama.join(', ') : 'Tidak ada';
|
||||
document.getElementById('icd10Lama').textContent = icd10Lama.length > 0 ? icd10Lama.join(', ') : 'Tidak ada';
|
||||
document.getElementById('icd9Baru').textContent = 'Belum ada data baru';
|
||||
document.getElementById('icd10Baru').textContent = 'Belum ada data baru';
|
||||
|
||||
// INACBG Lama
|
||||
const existingRI = currentEditingBilling.inacbg_ri || [];
|
||||
const existingRJ = currentEditingBilling.inacbg_rj || [];
|
||||
const inacbgRILamaEl = document.getElementById('inacbgRILama');
|
||||
const inacbgRJLamaEl = document.getElementById('inacbgRJLama');
|
||||
const totalKlaimLamaEl = document.getElementById('totalKlaimLama');
|
||||
|
||||
// Debug: log untuk cek data yang diterima
|
||||
console.log('=== DEBUG TOTAL KLAIM LAMA ===');
|
||||
console.log('Current editing billing:', currentEditingBilling);
|
||||
console.log('total_klaim:', currentEditingBilling.total_klaim);
|
||||
console.log('Total_Klaim:', currentEditingBilling.Total_Klaim);
|
||||
console.log('total_klaim_lama:', currentEditingBilling.total_klaim_lama);
|
||||
console.log('Total klaim lama (processed):', totalKlaimLama);
|
||||
console.log('All keys in billing object:', Object.keys(currentEditingBilling));
|
||||
console.log('================================');
|
||||
|
||||
if (existingRI.length > 0) {
|
||||
inacbgRILamaEl.innerHTML = `<strong>RI:</strong> ${existingRI.join(', ')}`;
|
||||
} else {
|
||||
inacbgRILamaEl.textContent = 'RI: Tidak ada';
|
||||
}
|
||||
|
||||
if (existingRJ.length > 0) {
|
||||
inacbgRJLamaEl.innerHTML = `<strong>RJ:</strong> ${existingRJ.join(', ')}`;
|
||||
} else {
|
||||
inacbgRJLamaEl.textContent = 'RJ: Tidak ada';
|
||||
}
|
||||
|
||||
// Tampilkan total klaim lama (selalu tampilkan, meskipun 0)
|
||||
totalKlaimLamaEl.textContent = `Total Klaim Lama: Rp ${totalKlaimLama.toLocaleString('id-ID')}`;
|
||||
|
||||
// Set total klaim lama di input
|
||||
document.getElementById('totalKlaimLamaInput').value = totalKlaimLama.toFixed(0);
|
||||
|
||||
// Set tanggal keluar jika ada
|
||||
// (akan diisi oleh admin, jadi kosong dulu)
|
||||
document.getElementById('tanggalKeluar').value = '';
|
||||
|
||||
// Reset INACBG form
|
||||
inacbgCodes = [];
|
||||
isManualInacbgMode = false;
|
||||
document.getElementById('inacbgCode').value = '';
|
||||
document.getElementById('inacbgCode').disabled = true;
|
||||
document.getElementById('inacbgCode').classList.remove('d-none');
|
||||
document.getElementById('inacbgCodeManual').value = '';
|
||||
document.getElementById('inacbgCodeManual').classList.add('d-none');
|
||||
document.getElementById('inacbgCode').innerHTML = '<option value="">-- Pilih Tipe INACBG Dulu --</option>';
|
||||
document.getElementById('tipeInacbg').value = '';
|
||||
document.getElementById('totalKlaim').value = '0';
|
||||
document.getElementById('codeList').innerHTML = '<small class="text-muted">Belum ada kode baru</small>';
|
||||
document.getElementById('totalKlaimAkhir').value = totalKlaimLama.toFixed(0);
|
||||
document.getElementById('formAlert').classList.add('d-none');
|
||||
|
||||
// Update billing sign display awal
|
||||
updateBillingSignDisplay();
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('editModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Toggle between dropdown and manual input
|
||||
function toggleInacbgInput() {
|
||||
isManualInacbgMode = !isManualInacbgMode;
|
||||
const codeSelect = document.getElementById('inacbgCode');
|
||||
const codeManual = document.getElementById('inacbgCodeManual');
|
||||
|
||||
if (isManualInacbgMode) {
|
||||
// Switch to manual input
|
||||
codeSelect.classList.add('d-none');
|
||||
codeManual.classList.remove('d-none');
|
||||
codeManual.focus();
|
||||
codeManual.value = '';
|
||||
} else {
|
||||
// Switch back to dropdown
|
||||
codeSelect.classList.remove('d-none');
|
||||
codeManual.classList.add('d-none');
|
||||
codeManual.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
function setupEventListeners() {
|
||||
// Tipe INACBG change
|
||||
document.getElementById('tipeInacbg').addEventListener('change', loadInacbgCodes);
|
||||
|
||||
// Add code button
|
||||
document.getElementById('addCodeBtn').addEventListener('click', addInacbgCode);
|
||||
|
||||
// INACBG form submit
|
||||
document.getElementById('inacbgForm').addEventListener('submit', submitInacbgForm);
|
||||
|
||||
// Search input
|
||||
document.getElementById('searchInput').addEventListener('input', searchBilling);
|
||||
|
||||
// Manual input enter key
|
||||
document.getElementById('inacbgCodeManual').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addInacbgCode();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load INACBG codes based on tipe
|
||||
async function loadInacbgCodes() {
|
||||
const tipe = document.getElementById('tipeInacbg').value;
|
||||
const codeSelect = document.getElementById('inacbgCode');
|
||||
|
||||
if (!tipe) {
|
||||
codeSelect.disabled = true;
|
||||
codeSelect.innerHTML = '<option value="">-- Pilih Tipe INACBG Dulu --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint = tipe === 'RI' ? '/tarifBPJSRawatInap' : '/tarifBPJSRawatJalan';
|
||||
|
||||
try {
|
||||
codeSelect.disabled = true;
|
||||
codeSelect.innerHTML = '<option value="">Memuat...</option>';
|
||||
|
||||
// Check cache first
|
||||
if (!tarifCache[tipe]) {
|
||||
const res = await fetch(`${API_BASE}${endpoint}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
tarifCache[tipe] = await res.json();
|
||||
}
|
||||
|
||||
const data = tarifCache[tipe] || [];
|
||||
const items = Array.isArray(data) ? data : [];
|
||||
|
||||
codeSelect.innerHTML = '<option value="">-- Pilih Kode --</option>';
|
||||
codeSelect.disabled = false;
|
||||
|
||||
items.forEach(item => {
|
||||
const option = document.createElement('option');
|
||||
// Use KodeINA as value and Deskripsi as display text
|
||||
option.value = item.KodeINA || item.kodeINA || item.KodeINA || '';
|
||||
option.textContent = item.Deskripsi || item.deskripsi || item.Deskripsi || '';
|
||||
|
||||
// If value is empty but we have other fields, try alternatives
|
||||
if (!option.value) {
|
||||
option.value = item.KodeINA_RJ || item.kodeINA_RJ || item.KodeINA_RI || item.kodeINA_RI || '';
|
||||
}
|
||||
|
||||
codeSelect.appendChild(option);
|
||||
});
|
||||
|
||||
console.log(`Loaded ${items.length} INACBG codes for type ${tipe}`);
|
||||
} catch (err) {
|
||||
console.error('Error loading INACBG codes:', err);
|
||||
codeSelect.disabled = true;
|
||||
codeSelect.innerHTML = `<option value="">Error: ${err.message}</option>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Get tarif for a code from cache or return 0
|
||||
function getTarifForCode(code, tipe, kelas = null) {
|
||||
let tarif = 0;
|
||||
const tarifData = tarifCache[tipe] || [];
|
||||
const tarifItem = tarifData.find(item => (item.KodeINA || item.kodeINA) === code);
|
||||
|
||||
if (tarifItem) {
|
||||
if (tipe === 'RI') {
|
||||
// Get tarif based on patient class
|
||||
if (!kelas) kelas = currentEditingBilling.Kelas;
|
||||
if (kelas === '1') {
|
||||
tarif = tarifItem.Kelas1 || 0;
|
||||
} else if (kelas === '2') {
|
||||
tarif = tarifItem.Kelas2 || 0;
|
||||
} else if (kelas === '3') {
|
||||
tarif = tarifItem.Kelas3 || 0;
|
||||
}
|
||||
} else if (tipe === 'RJ') {
|
||||
// Get tarif directly from TarifINACBG field
|
||||
tarif = tarifItem.TarifINACBG || tarifItem.tarif_inacbg || 0;
|
||||
}
|
||||
}
|
||||
|
||||
return tarif;
|
||||
}
|
||||
|
||||
// Add INACBG code (from dropdown or manual input)
|
||||
async function addInacbgCode() {
|
||||
const tipe = document.getElementById('tipeInacbg').value;
|
||||
|
||||
if (!tipe) {
|
||||
alert('Pilih tipe INACBG terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
|
||||
let code = '';
|
||||
let codeText = '';
|
||||
|
||||
if (isManualInacbgMode) {
|
||||
// Manual input mode
|
||||
const manualInput = document.getElementById('inacbgCodeManual').value.trim().toUpperCase();
|
||||
if (!manualInput) {
|
||||
alert('Masukkan kode INACBG');
|
||||
return;
|
||||
}
|
||||
code = manualInput;
|
||||
codeText = manualInput; // Manual input, use code as text
|
||||
} else {
|
||||
// Dropdown mode
|
||||
const codeSelect = document.getElementById('inacbgCode');
|
||||
const selectedOption = codeSelect.options[codeSelect.selectedIndex];
|
||||
code = codeSelect.value.trim();
|
||||
codeText = selectedOption.textContent.trim();
|
||||
|
||||
if (!code) {
|
||||
alert('Pilih kode INACBG terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (inacbgCodes.some(c => c.value === code)) {
|
||||
alert('Kode sudah ditambahkan');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get tarif for this code
|
||||
const tarif = getTarifForCode(code, tipe);
|
||||
|
||||
inacbgCodes.push({ value: code, text: codeText, tarif: tarif });
|
||||
|
||||
// Clear input/select
|
||||
if (isManualInacbgMode) {
|
||||
document.getElementById('inacbgCodeManual').value = '';
|
||||
} else {
|
||||
document.getElementById('inacbgCode').value = '';
|
||||
}
|
||||
|
||||
renderCodeList();
|
||||
calculateTotalKlaim(); // Update total after adding code
|
||||
}
|
||||
|
||||
// Render code list
|
||||
function renderCodeList() {
|
||||
const codeList = document.getElementById('codeList');
|
||||
codeList.innerHTML = '';
|
||||
|
||||
if (inacbgCodes.length === 0) {
|
||||
codeList.innerHTML = '<small class="text-muted">Belum ada kode baru</small>';
|
||||
return;
|
||||
}
|
||||
|
||||
inacbgCodes.forEach((codeObj, index) => {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'code-badge';
|
||||
const tarifDisplay = codeObj.tarif ? `(Rp${codeObj.tarif.toLocaleString('id-ID')})` : '';
|
||||
badge.innerHTML = `
|
||||
${codeObj.text || codeObj.value} ${tarifDisplay}
|
||||
<span class="remove-btn" onclick="removeInacbgCode(${index})">×</span>
|
||||
`;
|
||||
codeList.appendChild(badge);
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate total klaim dari kode baru SAJA (lama sudah tercatat di total_klaim backend)
|
||||
function calculateTotalKlaim() {
|
||||
const totalBaru = inacbgCodes.reduce((sum, code) => sum + (code.tarif || 0), 0);
|
||||
document.getElementById('totalKlaim').value = totalBaru.toFixed(0);
|
||||
|
||||
// Hitung total klaim akhir = lama + baru
|
||||
const totalKlaimLama = parseFloat(document.getElementById('totalKlaimLamaInput').value) || 0;
|
||||
const totalKlaimAkhir = totalKlaimLama + totalBaru;
|
||||
document.getElementById('totalKlaimAkhir').value = totalKlaimAkhir.toFixed(0);
|
||||
|
||||
// Update billing sign display berdasarkan total tarif RS kumulatif vs total klaim akhir
|
||||
updateBillingSignDisplay();
|
||||
}
|
||||
|
||||
// Remove INACBG code
|
||||
function removeInacbgCode(index) {
|
||||
inacbgCodes.splice(index, 1);
|
||||
renderCodeList();
|
||||
calculateTotalKlaim(); // Update total after removing code
|
||||
}
|
||||
|
||||
// Hitung billing sign berdasarkan rumus:
|
||||
// persentase = (total_tarif_rs / total_klaim_akhir) * 100
|
||||
function calculateBillingSign() {
|
||||
// totalTarifRs sudah kumulatif (lama + baru) dari backend
|
||||
const totalTarifRsStr = document.getElementById('modalTotalTarif').value.replace(/[^\d]/g, '');
|
||||
const totalTarifRs = parseFloat(totalTarifRsStr) || 0;
|
||||
|
||||
// total klaim akhir = lama + baru
|
||||
const totalKlaimAkhir = parseFloat(document.getElementById('totalKlaimAkhir').value) || 0;
|
||||
|
||||
if (totalTarifRs <= 0 || totalKlaimAkhir <= 0) {
|
||||
return { sign: null, percentage: 0 };
|
||||
}
|
||||
|
||||
const percentage = (totalTarifRs / totalKlaimAkhir) * 100;
|
||||
let sign = 'hijau';
|
||||
|
||||
if (percentage <= 25) {
|
||||
sign = 'hijau';
|
||||
} else if (percentage >= 26 && percentage <= 50) {
|
||||
sign = 'kuning';
|
||||
} else if (percentage >= 51 && percentage <= 75) {
|
||||
sign = 'orange';
|
||||
} else if (percentage >= 76) {
|
||||
sign = 'merah';
|
||||
}
|
||||
|
||||
return { sign, percentage };
|
||||
}
|
||||
|
||||
// Update tampilan billing sign di modal
|
||||
function updateBillingSignDisplay() {
|
||||
const container = document.getElementById('billingSignContainer');
|
||||
const badgeEl = document.getElementById('billingSignBadge');
|
||||
const textEl = document.getElementById('billingSignText');
|
||||
|
||||
if (!container || !badgeEl || !textEl) return;
|
||||
|
||||
const { sign, percentage } = calculateBillingSign();
|
||||
|
||||
if (!sign) {
|
||||
badgeEl.className = 'badge bg-secondary';
|
||||
badgeEl.textContent = '-';
|
||||
textEl.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const color = getBillingSignColor(sign);
|
||||
badgeEl.className = 'badge';
|
||||
badgeEl.style.backgroundColor = color;
|
||||
badgeEl.textContent = sign.toUpperCase();
|
||||
|
||||
const roundedPct = percentage.toFixed(2);
|
||||
textEl.textContent = `Tarif RS ≈ ${roundedPct}% dari BPJS`;
|
||||
}
|
||||
|
||||
// Format billing sign ke Title Case agar sesuai enum di DB
|
||||
function formatBillingSignValue(sign) {
|
||||
if (!sign) return '';
|
||||
const lower = sign.toLowerCase();
|
||||
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
||||
}
|
||||
|
||||
// Submit INACBG form
|
||||
async function submitInacbgForm(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const tipeInacbg = document.getElementById('tipeInacbg').value.trim();
|
||||
// total klaim BARU (tambahan); lama sudah tersimpan di backend
|
||||
const totalKlaimBaru = parseFloat(document.getElementById('totalKlaim').value) || 0;
|
||||
|
||||
// Validation
|
||||
if (!currentEditingBilling) {
|
||||
showAlert('danger', 'Data billing tidak ditemukan');
|
||||
return;
|
||||
}
|
||||
|
||||
if (inacbgCodes.length === 0) {
|
||||
showAlert('danger', 'Tambahkan minimal satu kode INACBG');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tipeInacbg) {
|
||||
showAlert('danger', 'Pilih tipe INACBG');
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalKlaimBaru === 0) {
|
||||
showAlert('danger', 'Total klaim tambahan tidak boleh 0');
|
||||
return;
|
||||
}
|
||||
|
||||
// Hitung billing sign berdasarkan total tarif RS dan total klaim
|
||||
const { sign: billingSign } = calculateBillingSign();
|
||||
const formattedBillingSign = formatBillingSignValue(billingSign);
|
||||
|
||||
// Ambil tanggal keluar jika diisi
|
||||
const tanggalKeluar = document.getElementById('tanggalKeluar').value.trim();
|
||||
|
||||
// Prepare payload
|
||||
const payload = {
|
||||
id_billing: currentEditingBilling.id_billing,
|
||||
tipe_inacbg: tipeInacbg,
|
||||
kode_inacbg: inacbgCodes.map(c => c.value), // Extract just the codes
|
||||
total_klaim: totalKlaimBaru, // Total klaim BARU saja (akan ditambahkan ke yang lama di backend)
|
||||
billing_sign: formattedBillingSign, // kirim billing sign sesuai enum DB
|
||||
tanggal_keluar: tanggalKeluar || '' // Tanggal keluar diisi oleh admin
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/admin/inacbg`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(result.error || result.message || 'Gagal menyimpan INACBG');
|
||||
}
|
||||
|
||||
showAlert('success', 'INACBG berhasil disimpan');
|
||||
setTimeout(() => {
|
||||
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
|
||||
loadBillingData();
|
||||
}, 1500);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error:', err);
|
||||
showAlert('danger', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Show alert in modal
|
||||
function showAlert(type, message) {
|
||||
const alert = document.getElementById('formAlert');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
alert.classList.remove('d-none');
|
||||
}
|
||||
|
||||
// Search billing
|
||||
function searchBilling(e) {
|
||||
const keyword = e.target.value.toLowerCase().trim();
|
||||
|
||||
if (keyword === '') {
|
||||
renderBillingTable();
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = billingData.filter(b =>
|
||||
(b.nama_pasien && b.nama_pasien.toLowerCase().includes(keyword)) ||
|
||||
(b.id_pasien && b.id_pasien.toString().includes(keyword))
|
||||
);
|
||||
|
||||
const tbody = document.getElementById('billingTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">Tidak ada hasil pencarian</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
filtered.forEach(billing => {
|
||||
const row = document.createElement('tr');
|
||||
const badgeColor = getBillingSignColor(billing.billing_sign);
|
||||
const badgeClass = getBillingSignBadgeClass(billing.billing_sign);
|
||||
|
||||
const totalTarif = billing.total_tarif_rs || 0;
|
||||
const totalKlaim = billing.total_klaim || 0;
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${billing.id_pasien || '-'}</td>
|
||||
<td>
|
||||
<a href="#" class="text-primary text-decoration-none" onclick="openEditModal(${billing.id_billing}); return false;">
|
||||
${billing.nama_pasien || '-'}
|
||||
</a>
|
||||
</td>
|
||||
<td>Rp ${Number(totalTarif).toLocaleString('id-ID')}</td>
|
||||
<td>Rp ${Number(totalKlaim).toLocaleString('id-ID')}</td>
|
||||
<td>
|
||||
<span class="billing-sign-badge ${badgeClass}" style="background-color: ${badgeColor};" title="${billing.billing_sign}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="openEditModal(${billing.id_billing})">
|
||||
✎ Edit
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
// Configuration
|
||||
const API_BASE = 'http://localhost:8081';
|
||||
let billingData = [];
|
||||
let currentEditingBilling = null;
|
||||
let inacbgCodes = [];
|
||||
let tarifCache = {}; // Cache for tarif data
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateCurrentDate();
|
||||
loadBillingData();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
// Update current date
|
||||
function updateCurrentDate() {
|
||||
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
||||
const today = new Date().toLocaleDateString('id-ID', options);
|
||||
document.getElementById('currentDate').textContent = today;
|
||||
}
|
||||
|
||||
// Load billing data from API
|
||||
async function loadBillingData() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/admin/billing`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const data = await res.json();
|
||||
billingData = data.data || [];
|
||||
console.log('Billing data loaded:', billingData);
|
||||
|
||||
renderBillingTable();
|
||||
renderRuanganSidebar();
|
||||
} catch (err) {
|
||||
console.error('Error loading billing data:', err);
|
||||
document.getElementById('billingTableBody').innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-danger">Gagal memuat data: ${err.message}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Render billing table
|
||||
function renderBillingTable() {
|
||||
const tbody = document.getElementById('billingTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (billingData.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted">Tidak ada data billing</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
billingData.forEach(billing => {
|
||||
const row = document.createElement('tr');
|
||||
const badgeClass = getBillingSignBadgeClass(billing.billing_sign);
|
||||
const badgeColor = getBillingSignColor(billing.billing_sign);
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${billing.id_pasien || '-'}</td>
|
||||
<td>
|
||||
<a href="#" class="text-primary text-decoration-none" onclick="openEditModal(${billing.id_billing}); return false;">
|
||||
${billing.nama_pasien || '-'}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="billing-sign-badge ${badgeClass}" style="background-color: ${badgeColor};" title="${billing.billing_sign}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="openEditModal(${billing.id_billing})">
|
||||
✎ Edit
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Get billing sign badge class and color
|
||||
function getBillingSignColor(billingSign) {
|
||||
switch (billingSign) {
|
||||
case 'hijau':
|
||||
return '#28a745';
|
||||
case 'kuning':
|
||||
return '#ffc107';
|
||||
case 'merah':
|
||||
case 'created':
|
||||
return '#dc3545';
|
||||
default:
|
||||
return '#6c757d';
|
||||
}
|
||||
}
|
||||
|
||||
function getBillingSignBadgeClass(billingSign) {
|
||||
switch (billingSign) {
|
||||
case 'hijau':
|
||||
return 'hijau';
|
||||
case 'kuning':
|
||||
return 'kuning';
|
||||
case 'merah':
|
||||
return 'merah';
|
||||
case 'created':
|
||||
return 'created';
|
||||
default:
|
||||
return 'created';
|
||||
}
|
||||
}
|
||||
|
||||
// Render ruangan sidebar
|
||||
function renderRuanganSidebar() {
|
||||
const uniqueRuangans = [...new Set(billingData.map(b => b.ruangan))];
|
||||
const ruanganList = document.getElementById('ruanganList');
|
||||
ruanganList.innerHTML = '';
|
||||
|
||||
if (uniqueRuangans.length === 0) {
|
||||
ruanganList.innerHTML = '<p class="text-muted">Tidak ada ruangan</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
uniqueRuangans.forEach((ruangan, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'ruangan-item';
|
||||
item.textContent = ruangan || `Ruangan ${index + 1}`;
|
||||
item.onclick = () => filterByRuangan(ruangan);
|
||||
ruanganList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter billing by ruangan
|
||||
function filterByRuangan(ruangan) {
|
||||
const filtered = billingData.filter(b => b.ruangan === ruangan);
|
||||
const tbody = document.getElementById('billingTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted">Tidak ada data untuk ruangan ini</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
filtered.forEach(billing => {
|
||||
const row = document.createElement('tr');
|
||||
const badgeColor = getBillingSignColor(billing.billing_sign);
|
||||
const badgeClass = getBillingSignBadgeClass(billing.billing_sign);
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${billing.id_pasien || '-'}</td>
|
||||
<td>
|
||||
<a href="#" class="text-primary text-decoration-none" onclick="openEditModal(${billing.id_billing}); return false;">
|
||||
${billing.nama_pasien || '-'}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="billing-sign-badge ${badgeClass}" style="background-color: ${badgeColor};" title="${billing.billing_sign}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="openEditModal(${billing.id_billing})">
|
||||
✎ Edit
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Open edit modal
|
||||
function openEditModal(billingId) {
|
||||
currentEditingBilling = billingData.find(b => b.id_billing === billingId);
|
||||
if (!currentEditingBilling) {
|
||||
alert('Data billing tidak ditemukan');
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate modal with billing data
|
||||
document.getElementById('modalNamaPasien').value = currentEditingBilling.nama_pasien || '';
|
||||
document.getElementById('modalIdPasien').value = currentEditingBilling.id_pasien || '';
|
||||
document.getElementById('modalKelas').value = currentEditingBilling.Kelas || '';
|
||||
document.getElementById('modalTindakan').value = (currentEditingBilling.tindakan_rs || []).join(', ') || '';
|
||||
document.getElementById('modalTotalTarif').value = currentEditingBilling.total_tarif_rs || '';
|
||||
document.getElementById('modalICD9').value = (currentEditingBilling.icd9 || []).join(', ') || '';
|
||||
document.getElementById('modalICD10').value = (currentEditingBilling.icd10 || []).join(', ') || '';
|
||||
|
||||
// Reset INACBG form
|
||||
inacbgCodes = [];
|
||||
document.getElementById('inacbgCode').value = '';
|
||||
document.getElementById('inacbgCode').disabled = true;
|
||||
document.getElementById('inacbgCode').innerHTML = '<option value="">-- Pilih Tipe INACBG Dulu --</option>';
|
||||
document.getElementById('tipeInacbg').value = '';
|
||||
document.getElementById('totalKlaim').value = '';
|
||||
document.getElementById('codeList').innerHTML = '';
|
||||
document.getElementById('formAlert').classList.add('d-none');
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('editModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
function setupEventListeners() {
|
||||
// Tipe INACBG change
|
||||
document.getElementById('tipeInacbg').addEventListener('change', loadInacbgCodes);
|
||||
|
||||
// Add code button
|
||||
document.getElementById('addCodeBtn').addEventListener('click', addInacbgCode);
|
||||
|
||||
// INACBG form submit
|
||||
document.getElementById('inacbgForm').addEventListener('submit', submitInacbgForm);
|
||||
|
||||
// Search input
|
||||
document.getElementById('searchInput').addEventListener('input', searchBilling);
|
||||
}
|
||||
|
||||
// Load INACBG codes based on tipe
|
||||
async function loadInacbgCodes() {
|
||||
const tipe = document.getElementById('tipeInacbg').value;
|
||||
const codeSelect = document.getElementById('inacbgCode');
|
||||
|
||||
if (!tipe) {
|
||||
codeSelect.disabled = true;
|
||||
codeSelect.innerHTML = '<option value="">-- Pilih Tipe INACBG Dulu --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint = tipe === 'RI' ? '/tarifBPJSRawatInap' : '/tarifBPJSRawatJalan';
|
||||
|
||||
try {
|
||||
codeSelect.disabled = true;
|
||||
codeSelect.innerHTML = '<option value="">Memuat...</option>';
|
||||
|
||||
// Check cache first
|
||||
if (!tarifCache[tipe]) {
|
||||
const res = await fetch(`${API_BASE}${endpoint}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
tarifCache[tipe] = await res.json();
|
||||
}
|
||||
|
||||
const data = tarifCache[tipe] || [];
|
||||
const items = Array.isArray(data) ? data : [];
|
||||
|
||||
codeSelect.innerHTML = '<option value="">-- Pilih Kode --</option>';
|
||||
codeSelect.disabled = false;
|
||||
|
||||
items.forEach(item => {
|
||||
const option = document.createElement('option');
|
||||
// Use KodeINA as value and Deskripsi as display text
|
||||
option.value = item.KodeINA || item.kodeINA || item.KodeINA || '';
|
||||
option.textContent = item.Deskripsi || item.deskripsi || item.Deskripsi || '';
|
||||
|
||||
// If value is empty but we have other fields, try alternatives
|
||||
if (!option.value) {
|
||||
option.value = item.KodeINA_RJ || item.kodeINA_RJ || item.KodeINA_RI || item.kodeINA_RI || '';
|
||||
}
|
||||
|
||||
codeSelect.appendChild(option);
|
||||
});
|
||||
|
||||
console.log(`Loaded ${items.length} INACBG codes for type ${tipe}`);
|
||||
} catch (err) {
|
||||
console.error('Error loading INACBG codes:', err);
|
||||
codeSelect.disabled = true;
|
||||
codeSelect.innerHTML = `<option value="">Error: ${err.message}</option>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add INACBG code
|
||||
async function addInacbgCode() {
|
||||
const codeSelect = document.getElementById('inacbgCode');
|
||||
const selectedOption = codeSelect.options[codeSelect.selectedIndex];
|
||||
const code = codeSelect.value.trim();
|
||||
const codeText = selectedOption.textContent.trim();
|
||||
const tipe = document.getElementById('tipeInacbg').value;
|
||||
|
||||
if (!code) {
|
||||
alert('Pilih kode INACBG terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
|
||||
if (inacbgCodes.some(c => c.value === code)) {
|
||||
alert('Kode sudah ditambahkan');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get tarif for this code
|
||||
let tarif = 0;
|
||||
const tarifData = tarifCache[tipe] || [];
|
||||
const tarifItem = tarifData.find(item => (item.KodeINA || item.kodeINA) === code);
|
||||
|
||||
if (tarifItem) {
|
||||
if (tipe === 'RI') {
|
||||
// Get tarif based on patient class
|
||||
const kelas = currentEditingBilling.Kelas;
|
||||
if (kelas === '1') {
|
||||
tarif = tarifItem.Kelas1 || 0;
|
||||
} else if (kelas === '2') {
|
||||
tarif = tarifItem.Kelas2 || 0;
|
||||
} else if (kelas === '3') {
|
||||
tarif = tarifItem.Kelas3 || 0;
|
||||
}
|
||||
} else if (tipe === 'RJ') {
|
||||
// Get tarif directly from TarifINACBG field
|
||||
tarif = tarifItem.TarifINACBG || tarifItem.tarif_inacbg || 0;
|
||||
}
|
||||
}
|
||||
|
||||
inacbgCodes.push({ value: code, text: codeText, tarif: tarif });
|
||||
codeSelect.value = '';
|
||||
renderCodeList();
|
||||
calculateTotalKlaim(); // Update total after adding code
|
||||
}
|
||||
|
||||
// Render code list
|
||||
function renderCodeList() {
|
||||
const codeList = document.getElementById('codeList');
|
||||
codeList.innerHTML = '';
|
||||
|
||||
if (inacbgCodes.length === 0) {
|
||||
codeList.innerHTML = '<p class="text-muted small">Belum ada kode</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
inacbgCodes.forEach((codeObj, index) => {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'code-badge';
|
||||
const tarifDisplay = codeObj.tarif ? `(Rp${codeObj.tarif.toLocaleString('id-ID')})` : '';
|
||||
badge.innerHTML = `
|
||||
${codeObj.text || codeObj.value} ${tarifDisplay}
|
||||
<span class="remove-btn" onclick="removeInacbgCode(${index})">×</span>
|
||||
`;
|
||||
codeList.appendChild(badge);
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate total klaim from selected codes
|
||||
function calculateTotalKlaim() {
|
||||
const total = inacbgCodes.reduce((sum, code) => sum + (code.tarif || 0), 0);
|
||||
document.getElementById('totalKlaim').value = total.toFixed(0);
|
||||
}
|
||||
|
||||
// Remove INACBG code
|
||||
function removeInacbgCode(index) {
|
||||
inacbgCodes.splice(index, 1);
|
||||
renderCodeList();
|
||||
calculateTotalKlaim(); // Update total after removing code
|
||||
}
|
||||
|
||||
// Submit INACBG form
|
||||
async function submitInacbgForm(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const tipeInacbg = document.getElementById('tipeInacbg').value.trim();
|
||||
const totalKlaim = parseFloat(document.getElementById('totalKlaim').value) || 0;
|
||||
|
||||
// Validation
|
||||
if (!currentEditingBilling) {
|
||||
showAlert('danger', 'Data billing tidak ditemukan');
|
||||
return;
|
||||
}
|
||||
|
||||
if (inacbgCodes.length === 0) {
|
||||
showAlert('danger', 'Tambahkan minimal satu kode INACBG');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tipeInacbg) {
|
||||
showAlert('danger', 'Pilih tipe INACBG');
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalKlaim === 0) {
|
||||
showAlert('danger', 'Total klaim tidak boleh 0');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare payload
|
||||
const payload = {
|
||||
id_billing: currentEditingBilling.id_billing,
|
||||
tipe_inacbg: tipeInacbg,
|
||||
kode_inacbg: inacbgCodes.map(c => c.value), // Extract just the codes
|
||||
total_klaim: totalKlaim,
|
||||
billing_sign: 'created' // or any status you want
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/admin/inacbg`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(result.error || result.message || 'Gagal menyimpan INACBG');
|
||||
}
|
||||
|
||||
showAlert('success', 'INACBG berhasil disimpan');
|
||||
setTimeout(() => {
|
||||
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
|
||||
loadBillingData();
|
||||
}, 1500);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error:', err);
|
||||
showAlert('danger', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Show alert in modal
|
||||
function showAlert(type, message) {
|
||||
const alert = document.getElementById('formAlert');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
alert.classList.remove('d-none');
|
||||
}
|
||||
|
||||
// Search billing
|
||||
function searchBilling(e) {
|
||||
const keyword = e.target.value.toLowerCase().trim();
|
||||
|
||||
if (keyword === '') {
|
||||
renderBillingTable();
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = billingData.filter(b =>
|
||||
(b.nama_pasien && b.nama_pasien.toLowerCase().includes(keyword)) ||
|
||||
(b.id_pasien && b.id_pasien.toString().includes(keyword))
|
||||
);
|
||||
|
||||
const tbody = document.getElementById('billingTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted">Tidak ada hasil pencarian</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
filtered.forEach(billing => {
|
||||
const row = document.createElement('tr');
|
||||
const badgeColor = getBillingSignColor(billing.billing_sign);
|
||||
const badgeClass = getBillingSignBadgeClass(billing.billing_sign);
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${billing.id_pasien || '-'}</td>
|
||||
<td>
|
||||
<a href="#" class="text-primary text-decoration-none" onclick="openEditModal(${billing.id_billing}); return false;">
|
||||
${billing.nama_pasien || '-'}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="billing-sign-badge ${badgeClass}" style="background-color: ${badgeColor};" title="${billing.billing_sign}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="openEditModal(${billing.id_billing})">
|
||||
✎ Edit
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
/* General Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.row {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
background-color: #f8f9fa;
|
||||
border-right: 1px solid #dee2e6;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #007bff;
|
||||
}
|
||||
|
||||
.sidebar-header h5 {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ruangan-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ruangan-item {
|
||||
padding: 10px 15px;
|
||||
background-color: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
color: #6c757d;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.ruangan-item:hover {
|
||||
background-color: #e7f3ff;
|
||||
border-color: #007bff;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.ruangan-item.active {
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header .text-muted {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 20px;
|
||||
padding: 10px 20px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Billing Table */
|
||||
.billing-table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.table tbody td {
|
||||
padding: 15px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Billing Sign Badge */
|
||||
.billing-sign-badge {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.billing-sign-badge.created {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.billing-sign-badge.kuning {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
|
||||
.billing-sign-badge.hijau {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.billing-sign-badge.merah {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-content {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.modal-header .modal-title {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-body h6 {
|
||||
font-weight: 600;
|
||||
color: #6c757d;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.modal-body .form-control {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.modal-body .form-control:focus {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.modal-body .form-select {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-body .form-select:focus {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Code List */
|
||||
.code-badge {
|
||||
display: inline-block;
|
||||
background-color: #e7f3ff;
|
||||
border: 1px solid #007bff;
|
||||
border-radius: 20px;
|
||||
padding: 6px 12px;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.9rem;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.code-badge .remove-btn {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
border-color: #0056b3;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #28a745;
|
||||
border-color: #28a745;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #218838;
|
||||
border-color: #218838;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.table thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table tbody td {
|
||||
display: block;
|
||||
text-align: right;
|
||||
padding-left: 50%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table tbody td:before {
|
||||
content: attr(data-label);
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
font-weight: 600;
|
||||
color: #6c757d;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Form Billing Pasien BPJS</title>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-header">
|
||||
<h2 class="mb-0 text-white">Form Billing Pasien BPJS</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="bpjsForm">
|
||||
|
||||
<!-- === DOKTER === -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nama Dokter</label>
|
||||
<div class="searchable-select-wrapper" id="wrapper_nama_dokter">
|
||||
<input type="text" class="searchable-select-input" id="nama_dokter" readonly placeholder="Dokter...">
|
||||
<span class="searchable-select-arrow">▼</span>
|
||||
<div class="searchable-select-dropdown" id="dropdown_nama_dokter">
|
||||
<div class="searchable-select-search">
|
||||
<input type="text" placeholder="Cari..." id="search_nama_dokter" autocomplete="off">
|
||||
</div>
|
||||
<div class="searchable-select-options" id="options_nama_dokter"></div>
|
||||
</div>
|
||||
</div>
|
||||
<select class="form-select d-none" id="select_nama_dokter" name="nama_dokter"></select>
|
||||
</div>
|
||||
|
||||
<!-- NAMA PASIEN -->
|
||||
<div class="mb-3 position-relative">
|
||||
<label class="form-label">Nama Pasien</label>
|
||||
<input type="text" class="form-control" id="nama_pasien" autocomplete="off">
|
||||
<div id="list_pasien" class="autocomplete-list"></div>
|
||||
<input type="hidden" id="id_pasien" name="id_pasien">
|
||||
</div>
|
||||
|
||||
<!-- Auto Fill atau Manual -->
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Jenis Kelamin</label>
|
||||
<input type="text" class="form-control" id="jenis_kelamin" placeholder="Auto fill atau isi manual">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Usia</label>
|
||||
<input type="number" class="form-control" id="usia" placeholder="Auto fill atau isi manual">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Ruangan</label>
|
||||
<div class="searchable-select-wrapper" id="wrapper_ruangan">
|
||||
<input type="text" class="searchable-select-input" id="ruangan" readonly placeholder="-- Pilih Ruangan --">
|
||||
<span class="searchable-select-arrow">▼</span>
|
||||
<div class="searchable-select-dropdown" id="dropdown_ruangan">
|
||||
<div class="searchable-select-search">
|
||||
<input type="text" placeholder="Cari..." id="search_ruangan" autocomplete="off">
|
||||
</div>
|
||||
<div class="searchable-select-options" id="options_ruangan"></div>
|
||||
</div>
|
||||
</div>
|
||||
<select class="form-select d-none" id="select_ruangan" name="ruangan"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Kelas</label>
|
||||
<select class="form-select" id="kelas" name="kelas">
|
||||
<option value="">-- Pilih Kelas --</option>
|
||||
<option value="1">Kelas 1</option>
|
||||
<option value="2">Kelas 2</option>
|
||||
<option value="3">Kelas 3</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Cara Bayar</label>
|
||||
<select class="form-select" id="cara_bayar" name="cara_bayar">
|
||||
<option value="">-- Pilih Cara Bayar --</option>
|
||||
<option value="BPJS">BPJS</option>
|
||||
<option value="UMUM">Umum</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Tanggal Keluar diisi oleh Admin Billing, bukan di form dokter -->
|
||||
</div>
|
||||
|
||||
<!-- TINDAKAN -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tindakan RS</label>
|
||||
<div class="searchable-select-wrapper multi-select" id="wrapper_tarif_rs">
|
||||
<div class="searchable-select-input" id="tarif_rs" placeholder="-- Pilih --">
|
||||
<input type="text" id="input_tarif_rs" placeholder="Tindakan..." autocomplete="off">
|
||||
</div>
|
||||
<span class="searchable-select-arrow">▼</span>
|
||||
<div class="searchable-select-dropdown" id="dropdown_tarif_rs">
|
||||
<div class="searchable-select-search">
|
||||
<input type="text" placeholder="Cari..." id="search_tarif_rs" autocomplete="off">
|
||||
</div>
|
||||
<div class="searchable-select-options" id="options_tarif_rs"></div>
|
||||
</div>
|
||||
</div>
|
||||
<select class="form-select d-none" id="select_tarif_rs" name="tarif_rs" multiple></select>
|
||||
</div>
|
||||
|
||||
<!-- ICD -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">ICD 9</label>
|
||||
<div class="searchable-select-wrapper multi-select" id="wrapper_icd9">
|
||||
<div class="searchable-select-input" id="icd9" placeholder="-- Pilih --">
|
||||
<input type="text" id="input_icd9" placeholder="-- Pilih --" autocomplete="off">
|
||||
</div>
|
||||
<span class="searchable-select-arrow">▼</span>
|
||||
<div class="searchable-select-dropdown" id="dropdown_icd9">
|
||||
<div class="searchable-select-search">
|
||||
<input type="text" placeholder="Cari..." id="search_icd9" autocomplete="off">
|
||||
</div>
|
||||
<div class="searchable-select-options" id="options_icd9"></div>
|
||||
</div>
|
||||
</div>
|
||||
<select class="form-select d-none" id="select_icd9" name="icd9" multiple></select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">ICD 10</label>
|
||||
<div class="searchable-select-wrapper multi-select" id="wrapper_icd10">
|
||||
<div class="searchable-select-input" id="icd10" placeholder="-- Pilih --">
|
||||
<input type="text" id="input_icd10" placeholder="-- Pilih --" autocomplete="off">
|
||||
</div>
|
||||
<span class="searchable-select-arrow">▼</span>
|
||||
<div class="searchable-select-dropdown" id="dropdown_icd10">
|
||||
<div class="searchable-select-search">
|
||||
<input type="text" placeholder="Cari..." id="search_icd10" autocomplete="off">
|
||||
</div>
|
||||
<div class="searchable-select-options" id="options_icd10"></div>
|
||||
</div>
|
||||
</div>
|
||||
<select class="form-select d-none" id="select_icd10" name="icd10" multiple></select>
|
||||
</div>
|
||||
|
||||
<!-- RIWAYAT BILLING AKTIF (TINDAKAN & ICD SEBELUMNYA) -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Riwayat Tindakan & ICD (Billing Aktif)</label>
|
||||
<div id="billing_history_info" class="small text-muted mb-2">
|
||||
Belum ada data yang dimuat. Pilih pasien untuk melihat riwayat.
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h6 class="fw-semibold">Tindakan RS</h6>
|
||||
<ul id="history_tindakan_rs" class="list-group small"></ul>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6 class="fw-semibold">ICD 9</h6>
|
||||
<ul id="history_icd9" class="list-group small"></ul>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6 class="fw-semibold">ICD 10</h6>
|
||||
<ul id="history_icd10" class="list-group small"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TOTAL TARIF RS -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Total Tarif RS</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Rp</span>
|
||||
<input type="text" class="form-control" id="total_tarif_rs" name="total_tarif_rs" readonly placeholder="0">
|
||||
</div>
|
||||
<small class="text-muted">Total akan dihitung otomatis berdasarkan tindakan yang dipilih</small>
|
||||
</div>
|
||||
|
||||
<div id="formAlert" class="alert d-none" role="alert"></div>
|
||||
|
||||
<div class="d-grid gap-2 mt-4">
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
💾 Simpan Data
|
||||
</button>
|
||||
<button type="button" id="saveDraftBtn" class="btn btn-outline-secondary btn-lg">💾 Save Draft</button>
|
||||
<button type="button" id="clearDraftBtn" class="btn btn-outline-danger btn-lg">🗑️ Clear Draft</button>
|
||||
<div id="draftStatus" class="align-self-center ms-2 text-muted" style="font-size:0.95rem"></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- App script: loads dropdowns, autocomplete and form handlers -->
|
||||
<script src="script.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,616 @@
|
||||
/* ============= GLOBAL STYLES ============= */
|
||||
:root {
|
||||
--primary-color: #0d6efd;
|
||||
--secondary-color: #6c757d;
|
||||
--success-color: #198754;
|
||||
--danger-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
--info-color: #0dcaf0;
|
||||
--light-bg: #f8f9fa;
|
||||
--border-color: #dee2e6;
|
||||
--shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--shadow-lg: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--border-radius: 0.5rem;
|
||||
--transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ============= CARD STYLES ============= */
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-lg);
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 1rem 2rem rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%);
|
||||
color: white;
|
||||
padding: 1.5rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ============= FORM STYLES ============= */
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-control:disabled {
|
||||
background-color: #e9ecef;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* ============= BUTTON STYLES ============= */
|
||||
.btn {
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.625rem 1.5rem;
|
||||
font-weight: 600;
|
||||
transition: var(--transition);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 6px rgba(13, 110, 253, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #0056b3 0%, #004085 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(13, 110, 253, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* ============= AUTOCOMPLETE STYLES ============= */
|
||||
.autocomplete-list {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
background: white;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
width: 100%;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
box-shadow: var(--shadow-lg);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.autocomplete-list.show {
|
||||
display: block;
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-item {
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.autocomplete-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.autocomplete-item:hover {
|
||||
background: linear-gradient(90deg, #e3f2fd 0%, #f5f5f5 100%);
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.autocomplete-item.text-muted {
|
||||
color: var(--secondary-color);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.autocomplete-item.text-danger {
|
||||
color: var(--danger-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ============= SEARCHABLE DROPDOWN STYLES ============= */
|
||||
.searchable-select-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.searchable-select-input {
|
||||
width: 100%;
|
||||
padding: 0.625rem 2.5rem 0.625rem 0.75rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
background-color: #fff;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.searchable-select-input:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.searchable-select-input:focus {
|
||||
border-color: var(--primary-color);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
|
||||
.searchable-select-input.searching {
|
||||
cursor: text;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.searchable-select-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1050;
|
||||
background: white;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
margin-top: 0.25rem;
|
||||
max-height: 250px;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
box-shadow: var(--shadow-lg);
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
.searchable-select-dropdown.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.searchable-select-search {
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.searchable-select-search input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.9rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.searchable-select-search input:focus {
|
||||
border-color: var(--primary-color);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.15);
|
||||
}
|
||||
|
||||
.searchable-select-options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.searchable-select-options::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.searchable-select-options::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.searchable-select-options::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.searchable-select-options::-webkit-scrollbar-thumb:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.searchable-select-option {
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: var(--transition);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.searchable-select-option:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.searchable-select-option:hover,
|
||||
.searchable-select-option.highlighted {
|
||||
background: linear-gradient(90deg, #e3f2fd 0%, #f5f5f5 100%);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.searchable-select-option.selected {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.searchable-select-option.selected::after {
|
||||
content: " ✓";
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.searchable-select-arrow {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
transition: transform 0.3s ease;
|
||||
color: var(--secondary-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.open .searchable-select-arrow {
|
||||
transform: translateY(-50%) rotate(180deg);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.searchable-select-no-results {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: var(--secondary-color);
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ============= MULTI-SELECT STYLES ============= */
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input {
|
||||
min-height: 38px;
|
||||
max-height: 100px;
|
||||
padding: 0.25rem 2.5rem 0.25rem 0.5rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input::-webkit-scrollbar-thumb:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input:empty::before {
|
||||
content: attr(placeholder);
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.selected-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%);
|
||||
color: white;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
gap: 0.25rem;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 3px rgba(13, 110, 253, 0.3);
|
||||
max-width: 100%;
|
||||
word-break: break-word;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.selected-chip .chip-remove {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
padding: 0 0.25rem;
|
||||
border-radius: 0.125rem;
|
||||
transition: var(--transition);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.selected-chip .chip-remove:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input input {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0.375rem;
|
||||
background: transparent;
|
||||
cursor: text;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* ============= LOADING STYLES ============= */
|
||||
.loading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.spinner-border-sm {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-width: 0.15em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.select-loading {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.select-loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: translateY(-50%) rotate(0deg); }
|
||||
100% { transform: translateY(-50%) rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ============= RESPONSIVE STYLES ============= */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select,
|
||||
.searchable-select-input {
|
||||
font-size: 0.95rem;
|
||||
padding: 0.5rem 0.625rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.row > [class*="col-"] {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
body {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.searchable-select-dropdown {
|
||||
max-height: 250px;
|
||||
}
|
||||
|
||||
.autocomplete-list {
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============= UTILITY CLASSES ============= */
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.shadow-sm {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.shadow-lg {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
/* ============= ANIMATIONS ============= */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
animation: fadeIn 0.5s ease;
|
||||
}
|
||||
|
||||
/* ============= TOTAL TARIF RS STYLES ============= */
|
||||
#total_tarif_rs {
|
||||
font-weight: 700;
|
||||
color: #198754;
|
||||
font-size: 1.25rem;
|
||||
text-align: right;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background: linear-gradient(135deg, var(--success-color) 0%, #146c43 100%);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.input-group .form-control:focus {
|
||||
border-color: var(--success-color);
|
||||
box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);
|
||||
}
|
||||
|
||||
/* ============= FOCUS VISIBLE ============= */
|
||||
*:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ============= PRINT STYLES ============= */
|
||||
@media print {
|
||||
body {
|
||||
background: white;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: none;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.btn,
|
||||
.searchable-select-arrow,
|
||||
.autocomplete-list {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,789 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Backend Careit - Test Interface</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
background: #f5f5f5;
|
||||
padding: 15px 20px;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.endpoint-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.endpoint-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.endpoint-item label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
color: #555;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.endpoint-item input {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 25px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-list {
|
||||
background: #4CAF50;
|
||||
}
|
||||
|
||||
.btn-list:hover {
|
||||
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.response-area {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.response-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.response-header h3 {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-ok {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-loading {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.view-toggle button {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.view-toggle button.active {
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9), 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
#response,
|
||||
#tableResponse {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#tableResponse {
|
||||
display: none;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: white;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #eee;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: #f1f5ff;
|
||||
}
|
||||
|
||||
.health-check {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: #e8f5e9;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.health-status {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.health-ok {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.health-error {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🏥 Backend Careit</h1>
|
||||
<p>Test Interface untuk API Backend</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- Health Check -->
|
||||
<div class="health-check">
|
||||
<h2>Health Check</h2>
|
||||
<button onclick="checkHealth()" style="margin-top: 10px;">Cek Status Server</button>
|
||||
<div id="healthStatus" class="health-status"></div>
|
||||
</div>
|
||||
|
||||
<!-- Tarif BPJS Rawat Inap -->
|
||||
<div class="section">
|
||||
<div class="section-header">📋 Tarif BPJS Rawat Inap</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getTarifBPJSRawatInap()">List Semua Tarif</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Kode INA CBG:</label>
|
||||
<input type="text" id="kodeRawatInap" placeholder="Masukkan kode...">
|
||||
<button onclick="getTarifBPJSRawatInapByKode()">Cari by Kode</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tarif BPJS Rawat Jalan -->
|
||||
<div class="section">
|
||||
<div class="section-header">🚶 Tarif BPJS Rawat Jalan</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getTarifBPJSRawatJalan()">List Semua Tarif</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Kode INA CBG:</label>
|
||||
<input type="text" id="kodeRawatJalan" placeholder="Masukkan kode...">
|
||||
<button onclick="getTarifBPJSRawatJalanByKode()">Cari by Kode</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tarif RS -->
|
||||
<div class="section">
|
||||
<div class="section-header">🏨 Tarif RS</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getTarifRS()">List Semua Tarif</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Kode RS:</label>
|
||||
<input type="text" id="kodeRS" placeholder="Masukkan kode...">
|
||||
<button onclick="getTarifRSByKode()">Cari by Kode</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Kategori:</label>
|
||||
<input type="text" id="kategoriRS" placeholder="Masukkan kategori...">
|
||||
<button onclick="getTarifRSByKategori()">Cari by Kategori</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ICD9 -->
|
||||
<div class="section">
|
||||
<div class="section-header">🧬 Kode Tindakan ICD9</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getICD9()">List Semua Kode ICD9</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Filter (client side, berdasarkan teks):</label>
|
||||
<input type="text" id="filterICD9" placeholder="Ketik untuk filter di tabel..." oninput="filterICD9ClientSide()">
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
|
||||
Data diambil dari endpoint <code>/icd9</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON (misal: kode, deskripsi, dll).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ICD10 -->
|
||||
<div class="section">
|
||||
<div class="section-header">🩺 Kode Diagnosis ICD10</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getICD10()">List Semua Kode ICD10</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Filter (client side, berdasarkan teks):</label>
|
||||
<input type="text" id="filterICD10" placeholder="Ketik untuk filter di tabel..." oninput="filterICD10ClientSide()">
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
|
||||
Data diambil dari endpoint <code>/icd10</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dokter -->
|
||||
<div class="section">
|
||||
<div class="section-header">👨⚕️ Data Dokter</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getDokter()">List Semua Dokter</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Filter (client side, berdasarkan teks):</label>
|
||||
<input type="text" id="filterDokter" placeholder="Ketik untuk filter di tabel..." oninput="filterDokterClientSide()">
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
|
||||
Data diambil dari endpoint <code>/dokter</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ruangan -->
|
||||
<div class="section">
|
||||
<div class="section-header">🏥 Data Ruangan</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getRuangan()">List Semua Ruangan</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Filter (client side, berdasarkan teks):</label>
|
||||
<input type="text" id="filterRuangan" placeholder="Ketik untuk filter di tabel..." oninput="filterRuanganClientSide()">
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
|
||||
Data diambil dari endpoint <code>/ruangan</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Response Area -->
|
||||
<div class="response-area">
|
||||
<div class="response-header">
|
||||
<h3>Response:</h3>
|
||||
<span id="statusBadge" class="status-badge" style="display: none;"></span>
|
||||
</div>
|
||||
<div class="view-toggle">
|
||||
<button id="btnJsonView" class="active" onclick="setViewMode('json')">JSON</button>
|
||||
<button id="btnTableView" onclick="setViewMode('table')">Table (Auto)</button>
|
||||
</div>
|
||||
<div id="response"></div>
|
||||
<div id="tableResponse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://localhost:8081';
|
||||
|
||||
function showResponse(data, status = 'ok') {
|
||||
const responseDiv = document.getElementById('response');
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const statusBadge = document.getElementById('statusBadge');
|
||||
|
||||
// Tampilkan JSON mentah
|
||||
responseDiv.textContent = JSON.stringify(data, null, 2);
|
||||
|
||||
// Coba render tabel
|
||||
renderTable(data);
|
||||
|
||||
statusBadge.style.display = 'inline-block';
|
||||
statusBadge.className = 'status-badge status-' + status;
|
||||
statusBadge.textContent = status === 'ok' ? '✓ Success' : status === 'loading' ? '⏳ Loading...' : '✗ Error';
|
||||
}
|
||||
|
||||
function setViewMode(mode) {
|
||||
const responseDiv = document.getElementById('response');
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const btnJson = document.getElementById('btnJsonView');
|
||||
const btnTable = document.getElementById('btnTableView');
|
||||
|
||||
if (mode === 'json') {
|
||||
responseDiv.style.display = 'block';
|
||||
tableDiv.style.display = 'none';
|
||||
btnJson.classList.add('active');
|
||||
btnTable.classList.remove('active');
|
||||
} else {
|
||||
responseDiv.style.display = 'none';
|
||||
tableDiv.style.display = 'block';
|
||||
btnJson.classList.remove('active');
|
||||
btnTable.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function formatRupiah(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '';
|
||||
}
|
||||
// Convert to number
|
||||
const num = typeof value === 'string' ? parseFloat(value) : value;
|
||||
if (isNaN(num)) {
|
||||
return value;
|
||||
}
|
||||
// Format dengan pemisah ribuan
|
||||
return 'Rp ' + num.toLocaleString('id-ID', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
}
|
||||
|
||||
function isTarifColumn(key) {
|
||||
const lowerKey = key.toLowerCase();
|
||||
return lowerKey.includes('tarif') ||
|
||||
lowerKey.includes('harga') ||
|
||||
lowerKey.includes('kelas') ||
|
||||
lowerKey.includes('tarif_inacbg');
|
||||
}
|
||||
|
||||
function renderTable(data) {
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
|
||||
// Reset isi
|
||||
tableDiv.innerHTML = '';
|
||||
|
||||
// Hanya render tabel untuk array of objects
|
||||
if (!Array.isArray(data) || data.length === 0 || typeof data[0] !== 'object') {
|
||||
setViewMode('json');
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = Array.from(
|
||||
data.reduce((set, item) => {
|
||||
Object.keys(item || {}).forEach(k => set.add(k));
|
||||
return set;
|
||||
}, new Set())
|
||||
);
|
||||
|
||||
if (keys.length === 0) {
|
||||
setViewMode('json');
|
||||
return;
|
||||
}
|
||||
|
||||
const table = document.createElement('table');
|
||||
const thead = document.createElement('thead');
|
||||
const tbody = document.createElement('tbody');
|
||||
|
||||
const headerRow = document.createElement('tr');
|
||||
keys.forEach(key => {
|
||||
const th = document.createElement('th');
|
||||
th.textContent = key;
|
||||
headerRow.appendChild(th);
|
||||
});
|
||||
thead.appendChild(headerRow);
|
||||
|
||||
data.forEach(row => {
|
||||
const tr = document.createElement('tr');
|
||||
keys.forEach(key => {
|
||||
const td = document.createElement('td');
|
||||
const value = row && key in row ? row[key] : '';
|
||||
|
||||
// Format tarif dengan rupiah
|
||||
if (isTarifColumn(key) && (typeof value === 'number' || (typeof value === 'string' && !isNaN(value) && value !== ''))) {
|
||||
td.textContent = formatRupiah(value);
|
||||
td.style.textAlign = 'right';
|
||||
} else {
|
||||
td.textContent = typeof value === 'object' ? JSON.stringify(value) : value;
|
||||
}
|
||||
tr.appendChild(td);
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
table.appendChild(thead);
|
||||
table.appendChild(tbody);
|
||||
tableDiv.appendChild(table);
|
||||
|
||||
// Auto switch ke view tabel
|
||||
setViewMode('table');
|
||||
}
|
||||
|
||||
async function checkHealth() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/`);
|
||||
const data = await response.json();
|
||||
|
||||
const healthStatus = document.getElementById('healthStatus');
|
||||
if (response.ok) {
|
||||
healthStatus.textContent = '✓ Server Online';
|
||||
healthStatus.className = 'health-status health-ok';
|
||||
} else {
|
||||
healthStatus.textContent = '✗ Server Error';
|
||||
healthStatus.className = 'health-status health-error';
|
||||
}
|
||||
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
const healthStatus = document.getElementById('healthStatus');
|
||||
healthStatus.textContent = '✗ Connection Error';
|
||||
healthStatus.className = 'health-status health-error';
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifBPJSRawatInap() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifBPJSRawatInap`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifBPJSRawatInapByKode() {
|
||||
const kode = document.getElementById('kodeRawatInap').value;
|
||||
if (!kode) {
|
||||
alert('Silakan masukkan kode!');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifBPJS/${encodeURIComponent(kode)}`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifBPJSRawatJalan() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifBPJSRawatJalan`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifBPJSRawatJalanByKode() {
|
||||
const kode = document.getElementById('kodeRawatJalan').value;
|
||||
if (!kode) {
|
||||
alert('Silakan masukkan kode!');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifBPJSRawatJalan/${encodeURIComponent(kode)}`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifRS() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifRS`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifRSByKode() {
|
||||
const kode = document.getElementById('kodeRS').value;
|
||||
if (!kode) {
|
||||
alert('Silakan masukkan kode!');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifRS/${encodeURIComponent(kode)}`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifRSByKategori() {
|
||||
const kategori = document.getElementById('kategoriRS').value;
|
||||
if (!kategori) {
|
||||
alert('Silakan masukkan kategori!');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifRSByKategori/${encodeURIComponent(kategori)}`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getICD9() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/icd9`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function filterICD9ClientSide() {
|
||||
const input = document.getElementById('filterICD9');
|
||||
const filter = input.value.toLowerCase();
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const table = tableDiv.querySelector('table');
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
async function getICD10() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/icd10`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function filterICD10ClientSide() {
|
||||
const input = document.getElementById('filterICD10');
|
||||
const filter = input.value.toLowerCase();
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const table = tableDiv.querySelector('table');
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
async function getDokter() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/dokter`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function filterDokterClientSide() {
|
||||
const input = document.getElementById('filterDokter');
|
||||
const filter = input.value.toLowerCase();
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const table = tableDiv.querySelector('table');
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
async function getRuangan() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/ruangan`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function filterRuanganClientSide() {
|
||||
const input = document.getElementById('filterRuangan');
|
||||
const filter = input.value.toLowerCase();
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const table = tableDiv.querySelector('table');
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Auto check health on load
|
||||
window.onload = function() {
|
||||
checkHealth();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user