// ============= 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 = '
Tidak ada hasil
'; 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 = ` ${option.text} × `; 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 = ''; 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 = '
Mencari...
'; 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 = '
Tidak ada hasil
'; } 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 = '
Error: Gagal memload data
'; } } // 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', ``); return; } let originalBtnHtml = ''; if (submitBtn) { originalBtnHtml = submitBtn.innerHTML; submitBtn.disabled = true; submitBtn.innerHTML = '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'); } }