Files
2026-02-06 14:22:35 +07:00

1406 lines
54 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============= CONFIGURATION =============
const API_BASE = "http://192.168.1.2:8081";
const FETCH_TIMEOUT = 10000;
// ============= UTILITY FUNCTIONS =============
function fetchWithTimeout(url, options = {}, timeout = FETCH_TIMEOUT) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), timeout)
)
]);
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// ============= SEARCHABLE DROPDOWN FUNCTIONS =============
function initSearchableDropdown(selectId) {
const wrapper = document.getElementById(`wrapper_${selectId}`);
const input = document.getElementById(selectId);
const inputField = document.getElementById(`input_${selectId}`) || input;
const dropdown = document.getElementById(`dropdown_${selectId}`);
const searchInput = document.getElementById(`search_${selectId}`);
const optionsContainer = document.getElementById(`options_${selectId}`);
const hiddenSelect = document.getElementById(`select_${selectId}`);
if (!wrapper || !input || !dropdown || !searchInput || !optionsContainer) return;
const isMultiSelect = wrapper.classList.contains('multi-select');
let allOptions = [];
let filteredOptions = [];
let selectedIndex = -1;
let selectedValues = new Set();
// Toggle dropdown
input.addEventListener('click', function(e) {
// Don't open if clicking on chip remove button
if (e.target.classList.contains('chip-remove')) return;
e.stopPropagation();
const isOpen = wrapper.classList.contains('open');
closeAllDropdowns();
if (!isOpen) {
openDropdown();
}
});
// For multi-select, also handle input field click and search
if (isMultiSelect && inputField) {
inputField.addEventListener('click', function(e) {
e.stopPropagation();
const isOpen = wrapper.classList.contains('open');
closeAllDropdowns();
if (!isOpen) {
openDropdown();
}
});
// Allow typing in input field for multi-select
inputField.addEventListener('input', function() {
const keyword = this.value.toLowerCase().trim();
if (keyword) {
const isOpen = wrapper.classList.contains('open');
if (!isOpen) {
openDropdown();
}
searchInput.value = keyword;
filterOptions(keyword);
}
});
}
// Search functionality
searchInput.addEventListener('input', function() {
const keyword = this.value.toLowerCase().trim();
filterOptions(keyword);
});
// Prevent dropdown close when clicking inside
dropdown.addEventListener('click', function(e) {
e.stopPropagation();
});
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!wrapper.contains(e.target)) {
closeDropdown();
}
});
// Keyboard navigation
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (!wrapper.classList.contains('open')) {
openDropdown();
}
}
});
searchInput.addEventListener('keydown', function(e) {
if (e.key === 'ArrowDown') {
e.preventDefault();
highlightNext();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
highlightPrev();
} else if (e.key === 'Enter') {
e.preventDefault();
selectHighlighted();
} else if (e.key === 'Escape') {
closeDropdown();
}
});
function openDropdown() {
wrapper.classList.add('open');
dropdown.classList.add('show');
if (!isMultiSelect) {
input.classList.add('searching');
}
searchInput.value = '';
searchInput.focus();
filterOptions('');
}
function closeDropdown() {
wrapper.classList.remove('open');
dropdown.classList.remove('show');
if (!isMultiSelect) {
input.classList.remove('searching');
}
selectedIndex = -1;
}
function closeAllDropdowns() {
document.querySelectorAll('.searchable-select-wrapper').forEach(w => {
w.classList.remove('open');
w.querySelector('.searchable-select-dropdown').classList.remove('show');
w.querySelector('.searchable-select-input').classList.remove('searching');
});
}
function filterOptions(keyword) {
filteredOptions = keyword === ''
? allOptions
: allOptions.filter(opt => opt.text.toLowerCase().includes(keyword));
renderOptions();
}
function renderOptions() {
optionsContainer.innerHTML = '';
selectedIndex = -1;
if (filteredOptions.length === 0) {
optionsContainer.innerHTML = '<div class="searchable-select-no-results">Tidak ada hasil</div>';
return;
}
filteredOptions.forEach((option, index) => {
const div = document.createElement('div');
div.className = 'searchable-select-option';
div.textContent = option.text;
div.dataset.value = option.value;
div.dataset.index = index;
// Check if selected (for multi-select)
if (isMultiSelect && selectedValues.has(option.value)) {
div.classList.add('selected');
} else if (!isMultiSelect) {
const currentInput = document.getElementById(`input_${selectId}`) || input;
if (currentInput && currentInput.value === option.text) {
div.classList.add('selected');
}
}
div.addEventListener('click', function() {
selectOption(option.value, option.text);
});
div.addEventListener('mouseenter', function() {
highlightOption(index);
});
optionsContainer.appendChild(div);
});
}
function renderChips() {
if (!isMultiSelect) return;
// Clear input container
input.innerHTML = '';
// Add chips for selected items
selectedValues.forEach(value => {
const option = allOptions.find(opt => opt.value === value);
if (option) {
const chip = document.createElement('span');
chip.className = 'selected-chip';
chip.innerHTML = `
<span>${option.text}</span>
<span class="chip-remove" data-value="${value}">×</span>
`;
chip.querySelector('.chip-remove').addEventListener('click', function(e) {
e.stopPropagation();
removeSelection(value);
});
input.appendChild(chip);
}
});
// Add input field
const inputEl = document.createElement('input');
inputEl.type = 'text';
inputEl.id = `input_${selectId}`;
inputEl.placeholder = selectedValues.size === 0 ? input.getAttribute('placeholder') || '-- Pilih --' : '';
inputEl.autocomplete = 'off';
input.appendChild(inputEl);
// Add event listeners to new input field
inputEl.addEventListener('click', function(e) {
e.stopPropagation();
const isOpen = wrapper.classList.contains('open');
closeAllDropdowns();
if (!isOpen) {
openDropdown();
}
});
inputEl.addEventListener('input', function() {
const keyword = this.value.toLowerCase().trim();
if (keyword) {
const isOpen = wrapper.classList.contains('open');
if (!isOpen) {
openDropdown();
}
searchInput.value = keyword;
filterOptions(keyword);
}
});
}
function highlightOption(index) {
document.querySelectorAll(`#options_${selectId} .searchable-select-option`).forEach((opt, i) => {
opt.classList.remove('highlighted');
if (i === index) {
opt.classList.add('highlighted');
selectedIndex = index;
}
});
}
function highlightNext() {
if (selectedIndex < filteredOptions.length - 1) {
selectedIndex++;
highlightOption(selectedIndex);
scrollToHighlighted();
}
}
function highlightPrev() {
if (selectedIndex > 0) {
selectedIndex--;
highlightOption(selectedIndex);
scrollToHighlighted();
}
}
function scrollToHighlighted() {
const highlighted = document.querySelector(`#options_${selectId} .searchable-select-option.highlighted`);
if (highlighted) {
highlighted.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
}
function selectHighlighted() {
if (selectedIndex >= 0 && selectedIndex < filteredOptions.length) {
const option = filteredOptions[selectedIndex];
selectOption(option.value, option.text);
}
}
function selectOption(value, text) {
if (isMultiSelect) {
// Toggle selection for multi-select
if (selectedValues.has(value)) {
removeSelection(value);
} else {
selectedValues.add(value);
if (hiddenSelect) {
const option = document.createElement('option');
option.value = value;
option.textContent = text;
option.selected = true;
hiddenSelect.appendChild(option);
}
renderChips();
renderOptions(); // Update checkmarks
}
// Keep dropdown open for multi-select
searchInput.focus();
} else {
// Single select behavior
inputField.value = text;
input.classList.remove('searching');
if (hiddenSelect) {
hiddenSelect.value = value;
}
closeDropdown();
}
// Trigger change event
const event = new Event('change', { bubbles: true });
if (hiddenSelect) hiddenSelect.dispatchEvent(event);
// Jika ini adalah tarif_rs, hitung ulang total
if (selectId === 'tarif_rs') {
calculateTotalTarifRS();
}
}
function removeSelection(value) {
selectedValues.delete(value);
if (hiddenSelect) {
const option = hiddenSelect.querySelector(`option[value="${value}"]`);
if (option) option.remove();
}
renderChips();
renderOptions(); // Update checkmarks
// Jika ini adalah tarif_rs, hitung ulang total
if (selectId === 'tarif_rs') {
calculateTotalTarifRS();
}
}
// Public function to set options
return {
setOptions: function(options) {
allOptions = options;
filteredOptions = options;
if (isMultiSelect) {
renderChips();
}
renderOptions();
},
getValue: function() {
if (isMultiSelect) {
return Array.from(selectedValues);
}
return hiddenSelect ? hiddenSelect.value : '';
},
setValue: function(value) {
if (isMultiSelect) {
// For multi-select, value should be an array
if (Array.isArray(value)) {
selectedValues = new Set(value);
if (hiddenSelect) {
hiddenSelect.innerHTML = '';
value.forEach(val => {
const option = allOptions.find(opt => opt.value === val);
if (option) {
const optEl = document.createElement('option');
optEl.value = option.value;
optEl.textContent = option.text;
optEl.selected = true;
hiddenSelect.appendChild(optEl);
}
});
}
renderChips();
renderOptions();
}
// Jika ini adalah tarif_rs, hitung ulang total setelah set value
if (selectId === 'tarif_rs') {
setTimeout(() => calculateTotalTarifRS(), 100);
}
} else {
// Untuk single-select, cari option berdasarkan value atau text
let option = allOptions.find(opt => opt.value === value || opt.value === value.toString());
// Jika tidak ditemukan berdasarkan value, coba cari berdasarkan text
if (!option) {
option = allOptions.find(opt => opt.text === value || opt.text === value.toString());
}
if (option) {
// Update input element langsung
input.value = option.text;
if (hiddenSelect) {
hiddenSelect.value = option.value;
}
console.log(`[${selectId}] setValue: Set to "${option.text}" (value: "${option.value}")`);
} else {
// Jika option tidak ditemukan, coba set langsung ke input sebagai fallback
console.warn(`[${selectId}] setValue: Option not found for value "${value}", setting directly to input`);
input.value = value;
if (hiddenSelect) {
// Coba cari di hidden select
const hiddenOption = Array.from(hiddenSelect.options).find(opt =>
opt.text === value || opt.value === value || opt.text === value.toString() || opt.value === value.toString()
);
if (hiddenOption) {
hiddenSelect.value = hiddenOption.value;
}
}
}
// Jika ini adalah tarif_rs, hitung ulang total setelah set value
if (selectId === 'tarif_rs') {
setTimeout(() => calculateTotalTarifRS(), 100);
}
}
}
};
}
// Store dropdown instances
const dropdownInstances = {};
// ============= LOADER DROPDOWN =============
async function loadSelect(url, selectId, labelField) {
const input = document.getElementById(selectId);
const hiddenSelect = document.getElementById(`select_${selectId}`);
if (!input && !hiddenSelect) return;
// Initialize searchable dropdown if not already
if (!dropdownInstances[selectId]) {
dropdownInstances[selectId] = initSearchableDropdown(selectId);
}
// Show loading
if (input) {
input.value = 'Memuat...';
input.disabled = true;
}
try {
const res = await fetchWithTimeout(url);
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const data = await res.json();
// Debug: log struktur data untuk troubleshooting
if (data.length > 0) {
console.log(`[${selectId}] Data structure:`, Object.keys(data[0]));
console.log(`[${selectId}] Looking for field: "${labelField}"`);
console.log(`[${selectId}] First item:`, data[0]);
}
// Helper function untuk mendapatkan field value (case-insensitive dan flexible)
function getFieldValue(item, fieldName) {
// KHUSUS untuk tarif_rs: HANYA gunakan "Deskripsi", JANGAN pakai "KodeRS" (ID)
if (selectId === 'tarif_rs') {
// Coba langsung "Deskripsi"
if (item['Deskripsi'] !== undefined && item['Deskripsi'] !== null && item['Deskripsi'] !== '') {
return item['Deskripsi'];
}
// Coba case-insensitive untuk "Deskripsi"
const keys = Object.keys(item);
const deskripsiKey = keys.find(k => k.toLowerCase() === 'deskripsi');
if (deskripsiKey && item[deskripsiKey] !== null && item[deskripsiKey] !== '') {
return item[deskripsiKey];
}
// JANGAN fallback ke field lain untuk tarif_rs (terutama KodeRS)
return null;
}
// KHUSUS untuk ruangan: tampilkan hanya Nama_Ruangan
if (selectId === 'ruangan') {
const namaRuangan = item['Nama_Ruangan'] || '';
return namaRuangan || null;
}
// Untuk dropdown lain, gunakan logic normal
// Coba langsung
if (item[fieldName] !== undefined && item[fieldName] !== null && item[fieldName] !== '') {
return item[fieldName];
}
// Coba case-insensitive
const lowerField = fieldName.toLowerCase();
const keys = Object.keys(item);
const foundKey = keys.find(k => k.toLowerCase() === lowerField);
if (foundKey && item[foundKey] !== null && item[foundKey] !== '') {
return item[foundKey];
}
// Coba partial match (jika fieldName adalah "Tindakan_RS", cari yang mengandung "tindakan")
const partialMatch = keys.find(k => {
const kLower = k.toLowerCase();
const fieldLower = lowerField.replace(/_/g, '');
return kLower.includes(fieldLower) || fieldLower.includes(kLower);
});
if (partialMatch && item[partialMatch] !== null && item[partialMatch] !== '') {
console.log(`[${selectId}] Using partial match: "${partialMatch}" instead of "${fieldName}"`);
return item[partialMatch];
}
// Jika tidak ditemukan, coba ambil field pertama yang ada value
const firstValidKey = keys.find(k => item[k] !== null && item[k] !== '' && item[k] !== undefined);
if (firstValidKey) {
console.warn(`[${selectId}] Field "${fieldName}" not found, using "${firstValidKey}" instead`);
return item[firstValidKey];
}
return null;
}
// Build options array untuk searchable dropdown
const options = [];
// Clear ruanganDataList jika ini adalah load ruangan
if (selectId === 'ruangan') {
ruanganDataList = [];
}
// Clear tarifRSDataList jika ini adalah load tarif_rs
if (selectId === 'tarif_rs') {
tarifRSDataList = [];
}
data.forEach(item => {
const value = getFieldValue(item, labelField);
// Skip jika value kosong/null
if (!value || value.toString().trim() === '') {
return;
}
const valueStr = value.toString().trim();
options.push({
value: valueStr,
text: valueStr
});
// KHUSUS untuk ruangan: store data asli untuk lookup (dengan ID_Ruangan)
if (selectId === 'ruangan') {
ruanganDataList.push(item);
console.log(`[ruangan] Stored ruangan data:`, item);
}
// KHUSUS untuk tarif_rs: store data asli untuk lookup harga
if (selectId === 'tarif_rs') {
tarifRSDataList.push(item);
console.log(`[tarif_rs] Stored tarif RS data:`, item);
}
});
if (options.length === 0) {
console.warn(`[${selectId}] No valid data found! Field "${labelField}" may not exist.`);
if (input) {
input.value = 'Data tidak tersedia';
input.disabled = true;
}
} else {
// Set options ke searchable dropdown
if (dropdownInstances[selectId]) {
dropdownInstances[selectId].setOptions(options);
}
// Set options ke hidden select juga
if (hiddenSelect) {
hiddenSelect.innerHTML = '<option value="">-- Pilih --</option>';
options.forEach(opt => {
const option = document.createElement('option');
option.value = opt.value;
option.textContent = opt.text;
hiddenSelect.appendChild(option);
});
}
if (input) {
input.value = '';
input.disabled = false;
input.placeholder = '-- Pilih --';
}
console.log(`[${selectId}] Loaded ${options.length} items`);
// Jika ini adalah load ruangan dan ada pending ruangan ID, set sekarang
if (selectId === 'ruangan' && pendingRuanganId) {
console.log('Ruangan selesai di-load, setting pending ruangan ID:', pendingRuanganId);
setTimeout(() => {
setRuanganFromId(pendingRuanganId);
pendingRuanganId = null; // Clear pending
}, 100);
}
// Jika ini adalah load tarif_rs, hitung total setelah load selesai
if (selectId === 'tarif_rs') {
setTimeout(() => {
calculateTotalTarifRS();
}, 200);
}
}
} catch (error) {
console.error(`Error loading ${selectId}:`, error);
if (input) {
input.value = 'Error memuat data';
input.disabled = true;
}
}
}
// ============= LOAD SEMUA DROPDOWN =============
async function loadAllSelects() {
// Load semua dropdown secara paralel untuk performa lebih baik
await Promise.all([
loadSelect(`${API_BASE}/dokter`, 'nama_dokter', 'Nama_Dokter'),
// Untuk tarif_rs: gunakan field "Deskripsi" dari JSON response (bukan KodeRS/ID)
loadSelect(`${API_BASE}/tarifRS`, 'tarif_rs', 'Deskripsi'),
loadSelect(`${API_BASE}/icd9`, 'icd9', 'Prosedur'),
loadSelect(`${API_BASE}/icd10`, 'icd10', 'Diagnosa'),
loadSelect(`${API_BASE}/ruangan`, 'ruangan', 'Nama_Ruangan')
]);
}
// Initialize all searchable dropdowns
function initAllDropdowns() {
const dropdownIds = ['nama_dokter', 'tarif_rs', 'icd9', 'icd10', 'ruangan'];
dropdownIds.forEach(id => {
if (!dropdownInstances[id]) {
dropdownInstances[id] = initSearchableDropdown(id);
}
});
}
// ============= CALCULATE TOTAL TARIF RS =============
function calculateTotalTarifRS() {
const totalInput = document.getElementById('total_tarif_rs');
if (!totalInput) {
console.warn('total_tarif_rs input tidak ditemukan');
return;
}
// Ambil semua tindakan yang dipilih dari dropdown tarif_rs
const selectedTindakan = [];
if (dropdownInstances['tarif_rs']) {
const selectedValues = dropdownInstances['tarif_rs'].getValue();
if (Array.isArray(selectedValues)) {
selectedTindakan.push(...selectedValues);
}
}
// Jika tidak ada tindakan yang dipilih, set total ke 0
if (selectedTindakan.length === 0) {
totalInput.value = '0';
return;
}
// Hitung total dari setiap tindakan
let total = 0;
selectedTindakan.forEach(tindakan => {
// Cari data tarif RS berdasarkan Deskripsi (tindakan) - case insensitive
const tarifData = tarifRSDataList.find(t => {
const deskripsi = t.Deskripsi || t.deskripsi || t.Tindakan_RS || t.tindakan_rs || '';
return deskripsi.toString().trim().toLowerCase() === tindakan.toString().trim().toLowerCase();
});
if (tarifData) {
// Ambil harga - coba berbagai kemungkinan field name
// Dari model: Harga int `gorm:"column:Tarif_RS"`
// Jadi di JSON response kemungkinan fieldnya adalah "Tarif_RS" atau "Harga"
const harga = tarifData.Tarif_RS ||
tarifData.tarif_rs ||
tarifData.TarifRS ||
tarifData.tarifRS ||
tarifData.Harga ||
tarifData.harga ||
0;
// Convert ke number
const hargaNum = parseInt(harga) || 0;
total += hargaNum;
console.log(`Tindakan "${tindakan}": Rp ${hargaNum.toLocaleString('id-ID')}`);
} else {
console.warn(`Harga tidak ditemukan untuk tindakan: "${tindakan}"`);
console.log('Available tarif RS data (first 3):', tarifRSDataList.slice(0, 3).map(t => ({
Deskripsi: t.Deskripsi,
Tarif_RS: t.Tarif_RS,
Harga: t.Harga,
allKeys: Object.keys(t)
})));
}
});
// Format angka dengan pemisah ribuan (format Indonesia)
totalInput.value = total.toLocaleString('id-ID');
console.log(`Total Tarif RS: Rp ${total.toLocaleString('id-ID')}`);
}
// ============= AUTOCOMPLETE PASIEN =============
const inputPasien = document.getElementById('nama_pasien');
const listPasien = document.getElementById('list_pasien');
let searchTimeout;
let currentSearchAbortController = null;
async function searchPasien(keyword) {
// Cancel previous request jika masih pending
if (currentSearchAbortController) {
currentSearchAbortController.abort();
}
if (keyword.length < 2) {
listPasien.innerHTML = '';
listPasien.classList.remove('show');
return;
}
// Tampilkan loading
listPasien.innerHTML = '<div class="autocomplete-item text-center"><span class="spinner-border spinner-border-sm me-2"></span>Mencari...</div>';
listPasien.classList.add('show');
try {
// Buat AbortController untuk cancel request
currentSearchAbortController = new AbortController();
const res = await fetchWithTimeout(
`${API_BASE}/pasien/search?nama=${encodeURIComponent(keyword)}`,
{ signal: currentSearchAbortController.signal }
);
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const response = await res.json();
// Handle response format: bisa array langsung atau {data: [], status: "success"}
const data = Array.isArray(response) ? response : (response.data || []);
// Optimasi: gunakan DocumentFragment untuk batch DOM updates
listPasien.innerHTML = '';
if (data.length === 0) {
listPasien.innerHTML = '<div class="autocomplete-item text-muted">Tidak ada hasil</div>';
} else {
const fragment = document.createDocumentFragment();
data.forEach(p => {
const div = document.createElement('div');
div.classList.add('autocomplete-item');
div.textContent = p.Nama_Pasien;
div.onclick = () => fillPasien(p);
fragment.appendChild(div);
});
listPasien.appendChild(fragment);
}
} catch (error) {
if (error.name === 'AbortError') {
// Request dibatalkan, ignore
return;
}
console.error('Error searching pasien:', error);
listPasien.innerHTML = '<div class="autocomplete-item text-danger">Error: Gagal memload data</div>';
}
}
// Debounce autocomplete dengan delay 300ms
const debouncedSearchPasien = debounce(searchPasien, 300);
inputPasien.addEventListener('input', function () {
debouncedSearchPasien(this.value.trim());
});
// Hide autocomplete saat klik di luar
document.addEventListener('click', function(e) {
if (!inputPasien.contains(e.target) && !listPasien.contains(e.target)) {
listPasien.classList.remove('show');
}
});
let lastSelectedPasienName = null;
let ruanganDataList = []; // Store semua data ruangan untuk lookup
let pendingRuanganId = null; // Store ruangan ID yang perlu di-set setelah load selesai
let tarifRSDataList = []; // Store semua data tarif RS untuk lookup harga
// ============= RIWAYAT BILLING AKTIF (TINDAKAN & ICD) =============
function clearBillingHistory() {
const infoEl = document.getElementById('billing_history_info');
const ulTindakan = document.getElementById('history_tindakan_rs');
const ulICD9 = document.getElementById('history_icd9');
const ulICD10 = document.getElementById('history_icd10');
if (infoEl) {
infoEl.textContent = 'Belum ada data yang dimuat. Pilih pasien untuk melihat riwayat.';
infoEl.classList.remove('text-danger');
infoEl.classList.add('text-muted');
}
if (ulTindakan) ulTindakan.innerHTML = '';
if (ulICD9) ulICD9.innerHTML = '';
if (ulICD10) ulICD10.innerHTML = '';
}
async function loadBillingAktifHistory(namaPasien) {
const infoEl = document.getElementById('billing_history_info');
const ulTindakan = document.getElementById('history_tindakan_rs');
const ulICD9 = document.getElementById('history_icd9');
const ulICD10 = document.getElementById('history_icd10');
if (!namaPasien || !infoEl || !ulTindakan || !ulICD9 || !ulICD10) {
return;
}
ulTindakan.innerHTML = '';
ulICD9.innerHTML = '';
ulICD10.innerHTML = '';
infoEl.textContent = 'Memuat riwayat billing aktif...';
infoEl.classList.remove('text-muted', 'text-danger');
try {
const res = await fetchWithTimeout(
`${API_BASE}/billing/aktif?nama_pasien=${encodeURIComponent(namaPasien)}`
);
if (res.status === 404) {
infoEl.textContent = 'Tidak ada billing aktif untuk pasien ini.';
infoEl.classList.remove('text-danger');
infoEl.classList.add('text-muted');
return;
}
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const body = await res.json().catch(() => ({}));
const data = body.data || {};
const tindakan = Array.isArray(data.tindakan_rs) ? data.tindakan_rs : [];
const icd9 = Array.isArray(data.icd9) ? data.icd9 : [];
const icd10 = Array.isArray(data.icd10) ? data.icd10 : [];
if (tindakan.length === 0 && icd9.length === 0 && icd10.length === 0) {
infoEl.textContent = 'Belum ada tindakan atau ICD yang tercatat pada billing aktif.';
infoEl.classList.remove('text-danger');
infoEl.classList.add('text-muted');
return;
}
infoEl.textContent = 'Menampilkan riwayat dari billing aktif pasien ini.';
infoEl.classList.remove('text-danger', 'text-muted');
tindakan.forEach(t => {
const li = document.createElement('li');
li.className = 'list-group-item py-1';
li.textContent = t;
ulTindakan.appendChild(li);
});
icd9.forEach(i => {
const li = document.createElement('li');
li.className = 'list-group-item py-1';
li.textContent = i;
ulICD9.appendChild(li);
});
icd10.forEach(i => {
const li = document.createElement('li');
li.className = 'list-group-item py-1';
li.textContent = i;
ulICD10.appendChild(li);
});
} catch (error) {
console.error('Error loading billing history:', error);
infoEl.textContent = 'Error memuat riwayat billing. Coba lagi.';
infoEl.classList.remove('text-muted');
infoEl.classList.add('text-danger');
}
}
// Helper function untuk set ruangan dari ID atau Nama
function setRuanganFromId(ruanganIdOrNama) {
console.log('setRuanganFromId called with:', ruanganIdOrNama);
console.log('ruanganDataList length:', ruanganDataList.length);
if (!ruanganIdOrNama) {
console.warn('ruanganIdOrNama is empty');
return;
}
if (ruanganDataList.length === 0) {
console.warn('ruanganDataList masih kosong');
return;
}
const searchValue = ruanganIdOrNama.toString().trim();
console.log('Mencari ruangan dengan value:', searchValue);
// PRIORITAS 1: Cari berdasarkan NAMA RUANGAN dulu (karena p.Ruangan sekarang adalah nama, bukan ID)
// Coba exact match dulu (case-insensitive)
let ruanganFound = ruanganDataList.find(r => {
const namaRuangan = r.Nama_Ruangan || r.nama_ruangan || r.NamaRuangan || r.Nama || r.nama || '';
const namaRuanganTrimmed = namaRuangan.toString().trim();
return namaRuanganTrimmed.toLowerCase() === searchValue.toLowerCase();
});
if (ruanganFound) {
console.log('✓ Found ruangan by NAMA (exact match):', ruanganFound);
} else {
// Coba partial match (nama ruangan mengandung searchValue atau sebaliknya)
console.log('Tidak ditemukan exact match, mencoba partial match...');
ruanganFound = ruanganDataList.find(r => {
const namaRuangan = r.Nama_Ruangan || r.nama_ruangan || r.NamaRuangan || r.Nama || r.nama || '';
const namaRuanganTrimmed = namaRuangan.toString().trim();
const searchLower = searchValue.toLowerCase();
const namaLower = namaRuanganTrimmed.toLowerCase();
// Cek apakah searchValue ada di nama ruangan atau sebaliknya
return namaLower.includes(searchLower) || searchLower.includes(namaLower);
});
if (ruanganFound) {
console.log('✓ Found ruangan by NAMA (partial match):', ruanganFound);
} else {
// PRIORITAS 2: Jika tidak ditemukan berdasarkan nama, cari berdasarkan ID
console.log('Tidak ditemukan berdasarkan nama, mencoba berdasarkan ID...');
ruanganFound = ruanganDataList.find(r => {
// Coba ID_Ruangan (case-insensitive comparison)
if (r.ID_Ruangan && r.ID_Ruangan.toString().trim() === searchValue) {
return true;
}
// Coba ID (case-insensitive)
if (r.ID && r.ID.toString().trim() === searchValue) {
return true;
}
// Coba id (lowercase)
if (r.id && r.id.toString().trim() === searchValue) {
return true;
}
// Coba Ruangan_ID atau field lain yang mungkin
const keys = Object.keys(r);
const idKey = keys.find(k => {
const kLower = k.toLowerCase();
return (kLower.includes('id') && kLower.includes('ruangan')) ||
(kLower === 'id_ruangan');
});
if (idKey && r[idKey] && r[idKey].toString().trim() === searchValue) {
return true;
}
return false;
});
if (ruanganFound) {
console.log('✓ Found ruangan by ID:', ruanganFound);
}
}
}
if (ruanganFound) {
console.log('Found ruangan:', ruanganFound); // Debug
// Ambil nama ruangan - coba berbagai kemungkinan field name
const namaRuangan = ruanganFound.Nama_Ruangan ||
ruanganFound.nama_ruangan ||
ruanganFound.NamaRuangan ||
ruanganFound.Nama ||
ruanganFound.nama ||
null;
if (namaRuangan) {
console.log('Setting ruangan nama:', namaRuangan);
// Set langsung ke input field sebagai fallback utama
const inputRuangan = document.getElementById('ruangan');
if (inputRuangan) {
inputRuangan.value = namaRuangan;
console.log('Input ruangan langsung di-set ke:', namaRuangan);
}
// Set ke hidden select juga
const selectEl = document.getElementById('select_ruangan');
if (selectEl) {
// Cari option yang match dengan namaRuangan
const options = selectEl.options;
for (let i = 0; i < options.length; i++) {
if (options[i].text === namaRuangan || options[i].value === namaRuangan) {
selectEl.value = options[i].value;
console.log('Hidden select di-set ke:', options[i].value);
break;
}
}
}
// Pastikan dropdown instance sudah ready dan set value
if (dropdownInstances['ruangan']) {
// Gunakan setTimeout untuk memastikan DOM sudah siap
setTimeout(() => {
try {
dropdownInstances['ruangan'].setValue(namaRuangan);
console.log('setValue called for ruangan:', namaRuangan);
// Double check - jika setelah setValue input masih kosong, set lagi
setTimeout(() => {
const checkInput = document.getElementById('ruangan');
if (checkInput && !checkInput.value) {
console.warn('Input masih kosong setelah setValue, setting lagi...');
checkInput.value = namaRuangan;
}
}, 200);
} catch (error) {
console.error('Error calling setValue:', error);
// Fallback: set langsung ke input
if (inputRuangan) {
inputRuangan.value = namaRuangan;
}
}
}, 100);
} else {
console.warn('dropdownInstances[ruangan] belum ada, menggunakan fallback');
}
} else {
console.warn('Nama ruangan tidak ditemukan di data:', ruanganFound);
console.log('Available fields:', Object.keys(ruanganFound));
}
} else {
console.warn('Ruangan dengan ID/Nama', searchValue, 'tidak ditemukan di ruanganDataList');
console.log('Mencari ruangan dengan nama yang mengandung:', searchValue);
// Coba partial match sebagai fallback
const partialMatch = ruanganDataList.find(r => {
const namaRuangan = r.Nama_Ruangan || r.nama_ruangan || r.NamaRuangan || r.Nama || r.nama || '';
return namaRuangan.toString().toLowerCase().includes(searchValue.toLowerCase()) ||
searchValue.toLowerCase().includes(namaRuangan.toString().toLowerCase());
});
if (partialMatch) {
console.log('Found ruangan dengan partial match:', partialMatch);
const namaRuangan = partialMatch.Nama_Ruangan || partialMatch.nama_ruangan || partialMatch.NamaRuangan || partialMatch.Nama || partialMatch.nama || '';
if (namaRuangan) {
const inputRuangan = document.getElementById('ruangan');
if (inputRuangan) {
inputRuangan.value = namaRuangan;
}
if (dropdownInstances['ruangan']) {
setTimeout(() => {
dropdownInstances['ruangan'].setValue(namaRuangan);
}, 100);
}
return;
}
}
console.log('Available ruangan data (first 5):', ruanganDataList.slice(0, 5).map(r => ({
ID_Ruangan: r.ID_Ruangan,
Nama_Ruangan: r.Nama_Ruangan
})));
}
}
function fillPasien(p) {
console.log('=== FILLING PASIEN ===');
console.log('Full pasien data:', JSON.stringify(p, null, 2)); // Debug - log full structure
console.log('Pasien Ruangan field:', p.Ruangan, 'Type:', typeof p.Ruangan); // Debug
console.log('RuanganDataList length:', ruanganDataList.length); // Debug
if (ruanganDataList.length > 0) {
console.log('Sample ruangan data (first item):', ruanganDataList[0]); // Debug
console.log('Sample ruangan ID_Ruangan:', ruanganDataList[0].ID_Ruangan); // Debug
console.log('Sample ruangan Nama_Ruangan:', ruanganDataList[0].Nama_Ruangan); // Debug
}
inputPasien.value = p.Nama_Pasien;
lastSelectedPasienName = p.Nama_Pasien; // Track nama yang dipilih
listPasien.innerHTML = '';
listPasien.classList.remove('show');
document.getElementById('id_pasien').value = p.ID_Pasien;
document.getElementById('jenis_kelamin').value = p.Jenis_Kelamin || '';
document.getElementById('usia').value = p.Usia || '';
// Set Kelas
if (p.Kelas) {
console.log('Setting kelas:', p.Kelas); // Debug
document.getElementById('kelas').value = p.Kelas;
}
// Set Ruangan - setRuanganFromId sekarang bisa handle baik ID maupun Nama
if (p.Ruangan) {
console.log('=== SETTING RUANGAN ===');
console.log('Pasien punya Ruangan:', p.Ruangan, 'Type:', typeof p.Ruangan); // Debug
// Jika ruanganDataList masih kosong, simpan sebagai pending dan tunggu load selesai
if (ruanganDataList.length === 0) {
console.log('RuanganDataList masih kosong, menyimpan sebagai pending...');
pendingRuanganId = p.Ruangan;
// Juga coba retry beberapa kali sebagai fallback
let retryCount = 0;
const maxRetries = 10;
const retrySetRuangan = () => {
retryCount++;
if (ruanganDataList.length > 0) {
console.log('RuanganDataList sudah terisi, setting ruangan...');
setRuanganFromId(p.Ruangan);
pendingRuanganId = null; // Clear pending
} else if (retryCount < maxRetries) {
console.log(`Retry ${retryCount}/${maxRetries} untuk set ruangan...`);
setTimeout(retrySetRuangan, 200);
} else {
console.warn('RuanganDataList masih kosong setelah beberapa retry');
}
};
setTimeout(retrySetRuangan, 200);
} else {
// Langsung set karena ruanganDataList sudah terisi
// setRuanganFromId akan otomatis cari berdasarkan nama dulu, lalu ID
setRuanganFromId(p.Ruangan);
}
}
// Setelah pasien dipilih, muat riwayat billing aktif (tindakan & ICD sebelumnya)
loadBillingAktifHistory(p.Nama_Pasien);
}
// Deteksi jika user mengubah nama pasien - clear ID jika berbeda
inputPasien.addEventListener('change', function() {
// Jika nama sekarang berbeda dengan yang dipilih, clear ID dan field terkait
if (lastSelectedPasienName && this.value.trim() !== lastSelectedPasienName) {
document.getElementById('id_pasien').value = '';
// Clear juga field ruangan dan kelas
if (dropdownInstances['ruangan']) {
dropdownInstances['ruangan'].setValue('');
}
document.getElementById('kelas').value = '';
clearBillingHistory();
}
});
// Load saat DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
initAllDropdowns();
loadAllSelects();
initBillingFormSubmission();
});
} else {
initAllDropdowns();
loadAllSelects();
initBillingFormSubmission();
}
// ============= FORM SUBMISSION =============
function initBillingFormSubmission() {
const form = document.getElementById('bpjsForm');
if (!form) return;
const alertBox = document.getElementById('formAlert');
const submitBtn = form.querySelector('button[type="submit"]');
function showFormAlert(type, message) {
if (!alertBox) return;
alertBox.className = `alert alert-${type}`;
alertBox.innerHTML = message;
alertBox.classList.remove('d-none');
}
function hideFormAlert() {
if (!alertBox) return;
alertBox.classList.add('d-none');
alertBox.textContent = '';
}
function getDropdownValue(id) {
if (dropdownInstances[id]) {
const value = dropdownInstances[id].getValue();
if (Array.isArray(value)) {
return value
.map(v => (v || '').toString().trim())
.filter(Boolean);
}
return (value || '').toString().trim();
}
const input = document.getElementById(id);
return input ? input.value.trim() : '';
}
function parseInteger(value) {
if (typeof value !== 'string') {
return Number.isFinite(value) ? value : 0;
}
const numeric = value.replace(/[^\d]/g, '');
return numeric ? parseInt(numeric, 10) : 0;
}
// Draft storage key
const DRAFT_KEY = 'billingDraft_v1';
function getFormDataForDraft() {
return {
nama_dokter: getDropdownValue('nama_dokter'),
nama_pasien: (document.getElementById('nama_pasien')?.value || '').trim(),
jenis_kelamin: (document.getElementById('jenis_kelamin')?.value || '').trim(),
usia: parseInteger(document.getElementById('usia')?.value || '0'),
ruangan: getDropdownValue('ruangan'),
kelas: (document.getElementById('kelas')?.value || '').trim(),
tindakan_rs: getDropdownValue('tarif_rs'),
// tanggal_keluar sekarang diisi Admin Billing, tidak perlu disimpan di draft FE dokter
tanggal_keluar: '',
icd9: getDropdownValue('icd9'),
icd10: getDropdownValue('icd10'),
cara_bayar: (document.getElementById('cara_bayar')?.value || '').trim(),
total_tarif_rs: parseInteger(document.getElementById('total_tarif_rs')?.value || '0'),
};
}
function saveDraftToStorage() {
try {
const data = getFormDataForDraft();
localStorage.setItem(DRAFT_KEY, JSON.stringify(data));
updateDraftStatus('Draft disimpan');
return true;
} catch (e) {
console.error('Gagal menyimpan draft:', e);
updateDraftStatus('Gagal menyimpan draft');
return false;
}
}
function clearDraftStorage() {
localStorage.removeItem(DRAFT_KEY);
updateDraftStatus('Draft dihapus');
}
function loadDraftFromStorage() {
const raw = localStorage.getItem(DRAFT_KEY);
if (!raw) return null;
try {
return JSON.parse(raw);
} catch (e) {
console.error('Draft parsing error:', e);
return null;
}
}
function updateDraftStatus(msg) {
const el = document.getElementById('draftStatus');
if (el) el.textContent = msg;
}
function applyDraftToForm(draft) {
if (!draft) return;
// Simple fields
document.getElementById('nama_pasien').value = draft.nama_pasien || '';
document.getElementById('jenis_kelamin').value = draft.jenis_kelamin || '';
document.getElementById('usia').value = draft.usia || '';
document.getElementById('kelas').value = draft.kelas || '';
document.getElementById('cara_bayar').value = draft.cara_bayar || '';
document.getElementById('total_tarif_rs').value = draft.total_tarif_rs ? draft.total_tarif_rs.toLocaleString('id-ID') : document.getElementById('total_tarif_rs').value;
// Dropdowns / multi-selects - may require retries until dropdownInstances loaded
const attempts = { count: 0 };
const tryApply = () => {
attempts.count++;
// nama_dokter
if (dropdownInstances['nama_dokter'] && draft.nama_dokter) {
try { dropdownInstances['nama_dokter'].setValue(draft.nama_dokter); } catch (e) { console.warn(e); }
}
// ruangan
if (dropdownInstances['ruangan'] && draft.ruangan) {
try { dropdownInstances['ruangan'].setValue(draft.ruangan); } catch (e) { console.warn(e); }
}
// tarif_rs (multi)
if (dropdownInstances['tarif_rs'] && Array.isArray(draft.tindakan_rs)) {
try { dropdownInstances['tarif_rs'].setValue(draft.tindakan_rs); } catch (e) { console.warn(e); }
}
// icd9, icd10
if (dropdownInstances['icd9'] && Array.isArray(draft.icd9)) {
try { dropdownInstances['icd9'].setValue(draft.icd9); } catch (e) { console.warn(e); }
}
if (dropdownInstances['icd10'] && Array.isArray(draft.icd10)) {
try { dropdownInstances['icd10'].setValue(draft.icd10); } catch (e) { console.warn(e); }
}
// If some dropdowns are not ready, retry a few times
const allReady = (!draft.nama_dokter || dropdownInstances['nama_dokter']) && (!draft.ruangan || dropdownInstances['ruangan']) && (!draft.tindakan_rs || dropdownInstances['tarif_rs']);
if (!allReady && attempts.count < 10) {
setTimeout(tryApply, 200);
} else {
updateDraftStatus('Draft dimuat');
}
};
tryApply();
}
form.addEventListener('submit', async function(event) {
event.preventDefault();
hideFormAlert();
const payload = {
nama_dokter: getDropdownValue('nama_dokter'),
nama_pasien: (document.getElementById('nama_pasien')?.value || '').trim(),
jenis_kelamin: (document.getElementById('jenis_kelamin')?.value || '').trim(),
usia: parseInteger(document.getElementById('usia')?.value || '0'),
ruangan: getDropdownValue('ruangan'),
kelas: (document.getElementById('kelas')?.value || '').trim(),
tindakan_rs: getDropdownValue('tarif_rs'),
// tanggal_keluar tidak dikirim dari FE dokter
tanggal_keluar: '',
icd9: getDropdownValue('icd9'),
icd10: getDropdownValue('icd10'),
cara_bayar: (document.getElementById('cara_bayar')?.value || '').trim(),
total_tarif_rs: parseInteger(document.getElementById('total_tarif_rs')?.value || '0'),
};
const errors = [];
if (!payload.nama_dokter) errors.push('Nama Dokter wajib dipilih.');
if (!payload.nama_pasien) errors.push('Nama Pasien wajib diisi.');
if (!payload.jenis_kelamin) errors.push('Jenis Kelamin wajib diisi.');
if (!payload.usia) errors.push('Usia wajib diisi.');
if (!payload.ruangan) errors.push('Ruangan wajib dipilih.');
if (!payload.kelas) errors.push('Kelas wajib dipilih.');
if (!Array.isArray(payload.tindakan_rs) || payload.tindakan_rs.length === 0) errors.push('Pilih minimal satu Tindakan RS.');
if (!Array.isArray(payload.icd9) || payload.icd9.length === 0) errors.push('Pilih minimal satu ICD 9.');
if (!Array.isArray(payload.icd10) || payload.icd10.length === 0) errors.push('Pilih minimal satu ICD 10.');
if (!payload.cara_bayar) errors.push('Cara Bayar wajib dipilih.');
if (errors.length > 0) {
showFormAlert('danger', `<ul class="mb-0"><li>${errors.join('</li><li>')}</li></ul>`);
return;
}
let originalBtnHtml = '';
if (submitBtn) {
originalBtnHtml = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Menyimpan...';
}
try {
const response = await fetchWithTimeout(`${API_BASE}/billing`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
const result = await response.json().catch(() => ({}));
if (!response.ok || result.status !== 'success') {
const message = result?.error || result?.message || 'Gagal menyimpan data billing.';
throw new Error(message);
}
showFormAlert('success', result.message || 'Billing berhasil dibuat.');
} catch (error) {
console.error('Error submitting billing form:', error);
showFormAlert('danger', error.message || 'Terjadi kesalahan saat menyimpan data.');
} finally {
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = originalBtnHtml || '💾 Simpan Data';
}
}
});
// Setup draft buttons
const saveDraftBtn = document.getElementById('saveDraftBtn');
const clearDraftBtn = document.getElementById('clearDraftBtn');
if (saveDraftBtn) {
saveDraftBtn.addEventListener('click', function() {
if (saveDraftToStorage()) {
showFormAlert('success', 'Draft berhasil disimpan.');
} else {
showFormAlert('danger', 'Gagal menyimpan draft. Cek console.');
}
});
}
if (clearDraftBtn) {
clearDraftBtn.addEventListener('click', function() {
clearDraftStorage();
showFormAlert('info', 'Draft dihapus.');
// optionally clear form fields as well
});
}
// Load draft on init
const existingDraft = loadDraftFromStorage();
if (existingDraft) {
applyDraftToForm(existingDraft);
} else {
updateDraftStatus('Tidak ada draft');
}
}