let button_read_mykad = document.getElementById("button_read_mykad"); let button_close_modal = document.querySelector("#daftar_pelanggan_mykad_reader button.btn.btn-danger"); let photo_mykad = document.querySelector("#photo-mykad"); let original_photo_loader = photo_mykad ? photo_mykad.src : ""; const MYKAD_READER_TIMEOUT_MS = 45000; const MYKAD_READER_DEFAULT_ENDPOINT = "http://localhost:17321/read"; let showLoader = () => { let pageLoader = document.getElementById('page_loader'); if (!pageLoader) { return; } pageLoader.removeAttribute('class'); pageLoader.classList.add('loading'); }; let hideLoader = () => { let pageLoader = document.getElementById('page_loader'); if (!pageLoader) { return; } pageLoader.removeAttribute('class'); }; function getMyKadReaderEndpoint() { let endpoint = (typeof mykad_reader_endpoint !== 'undefined' && mykad_reader_endpoint) ? String(mykad_reader_endpoint).trim() : ''; if (!endpoint || endpoint.includes('MYKADJSON') || endpoint.includes(':8085')) { return MYKAD_READER_DEFAULT_ENDPOINT; } return endpoint; } async function parseJsonResponse(response) { let text = await response.text(); let trimmed = (text || '').trim(); if (!trimmed) { throw new Error('Pembaca MyKad tidak memulangkan data. Sila cuba lagi.'); } if (trimmed.charAt(0) === '<') { throw new Error('Pembaca MyKad memulangkan halaman web, bukan data kad. Sila pastikan perkhidmatan mykad-helper sedang berjalan di localhost:17321.'); } try { return JSON.parse(trimmed); } catch (error) { throw new Error('Pembaca MyKad memulangkan data yang tidak sah. Sila cuba lagi.'); } } function getMyKadNumberWithoutHyphens(nric) { return String(nric || '').replace(/-/g, ''); } function titleCase(value) { return String(value || '') .toLowerCase() .split(' ') .filter(Boolean) .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } function getCodeFromValue(needle, haystack, fallback) { let normalised = titleCase(needle); let match = haystack.find(item => item[1].toLowerCase() === normalised.toLowerCase()); return match ? match[0] : fallback; } function mapGender(gender) { let value = String(gender || '').toLowerCase(); if (['male', 'lelaki', 'l', 'm'].includes(value)) { return 'Lelaki'; } if (['female', 'perempuan', 'p', 'f'].includes(value)) { return 'Perempuan'; } return gender || ''; } function formatDateOfBirth(dob) { if (!dob) { return ''; } let hyphenated = String(dob).includes('-') ? String(dob) : String(dob).replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3'); let [year, month, day] = hyphenated.split('-'); if (!year || !month || !day) { return dob; } return day + '/' + month + '/' + year; } function getAgeFromDateOfBirth(dob) { if (!dob) { return ''; } let hyphenated = String(dob).includes('-') ? String(dob) : String(dob).replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3'); let birthDate = new Date(hyphenated); if (Number.isNaN(birthDate.getTime())) { return ''; } let today = new Date(); let age = today.getFullYear() - birthDate.getFullYear(); let monthDiff = today.getMonth() - birthDate.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { age -= 1; } return age; } function getBumiputeraStatus(race) { const non_bumiputera_ethnicity = ['INDIA', 'CINA', 'INDIAN', 'CHINESE']; return !non_bumiputera_ethnicity.includes(String(race || '').toUpperCase()); } function joinAddress(data) { return [data.address1, data.address2, data.address3] .map(line => String(line || '').trim()) .filter(Boolean) .join(', '); } function mapReaderErrorCode(rawMessage) { const error_handler_dictionary = { '0x80100069': 'Tiada MyKad dikesan, sila masukkan MyKad untuk dibaca', '0x8010000C': 'Tiada MyKad dikesan, sila masukkan MyKad untuk dibaca', '0x80100066': 'MyKad mungkin dimasukkan terbalik, sila masukkan dengan betul', '0x80100067': 'MyKad tidak dapat dikesan. Sila cabut dan masukkan semula kad', '0x80100016': 'Gagal membaca MyKad. Sila pastikan kad dimasukkan dengan betul dan cuba lagi', '0x8010002E': 'Pembaca MyKad tidak dapat dikesan, sila periksa sambungan', '0x80100017': 'Pembaca MyKad tidak dapat dikesan, sila periksa sambungan', }; let message = String(rawMessage || ''); let codes = message.match(/0x[0-9a-fA-F]+/g) || []; let matched = codes.reverse().find(code => error_handler_dictionary[code]); return matched ? error_handler_dictionary[matched] : (message || null); } function getReaderErrorMessage(payload) { if (!payload) { return 'Pembaca MyKad tidak dapat membaca kad. Sila cuba lagi.'; } let raw = payload.message || payload.error || payload.detail || ''; return mapReaderErrorCode(raw) || 'Pembaca MyKad tidak dapat membaca kad. Sila pastikan MyKad dimasukkan dengan betul.'; } function getFetchErrorTitle(error) { if (error && error.name === 'AbortError') { return 'Masa menunggu pembaca MyKad telah tamat. Sila cuba lagi.'; } return 'Pembaca MyKad tidak dapat dikesan. Sila pastikan perkhidmatan pembaca MyKad sedang berjalan.'; } function getDisplayError(error) { if (!error) { return getFetchErrorTitle(error); } if (error.name === 'AbortError') { return getFetchErrorTitle(error); } let message = String(error.message || ''); if (!message || message.startsWith('Failed to fetch') || message.includes('NetworkError')) { return getFetchErrorTitle(error); } return message; } async function handleMyKadReader() { showLoader(); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), MYKAD_READER_TIMEOUT_MS); try { const response = await fetch(getMyKadReaderEndpoint(), { method: 'POST', headers: { 'Accept': 'application/json', }, signal: controller.signal, }); const payload = await parseJsonResponse(response); if (!response.ok) { throw new Error(getReaderErrorMessage(payload)); } if (!payload || payload.ok !== true || !payload.data) { throw new Error(getReaderErrorMessage(payload)); } await handleMyKadData(payload.data); } catch (error) { Swal.fire({ icon: 'error', title: getDisplayError(error), }); } finally { clearTimeout(timeoutId); hideLoader(); } } function handleMyKadData(data) { return checkIfMyKadExists(data.nric) .then(response => { let isResponseArray = Array.isArray(response); let resolved_response = isResponseArray ? response.pop() : response; if (!resolved_response || resolved_response === 'no-data') { populateDaftarPelangganModal(data); } else { let input_search_noic = document.querySelector("form[name='frmsearch'] input[name='noic']"); let form_search_noic = document.querySelector("form[name='frmsearch']"); input_search_noic.value = getMyKadNumberWithoutHyphens(data.nric); form_search_noic.submit(); } }) .catch(() => { populateDaftarPelangganModal(data); }); } function checkIfMyKadExists(myKadNumber) { let myKadNumberWithoutHyphens = getMyKadNumberWithoutHyphens(myKadNumber); let lookupUrl = new URL(wakil_caricustomer_endpoint, window.location.origin); lookupUrl.searchParams.set('icno', myKadNumberWithoutHyphens); const csrfToken = document.querySelector('input[name="_token"]'); return fetch(lookupUrl.toString(), { method: 'GET', headers: { "Accept": "application/json", "X-Requested-With": "XMLHttpRequest", "X-CSRF-Token": csrfToken ? csrfToken.value : "" }, credentials: "same-origin", }).then(async response => { let text = await response.text(); let trimmed = (text || '').trim(); if (!trimmed || trimmed.charAt(0) === '<') { return 'no-data'; } try { return JSON.parse(trimmed); } catch (error) { return 'no-data'; } }); } let populateDaftarPelangganModal = (data) => { $('#daftar_pelanggan_mykad_reader').modal('show'); let modal_form = document.querySelector("#daftar_pelanggan_mykad_reader .modal-body .row.form-group"); const religions = [ ['01', 'Islam'], ['02', 'Hindu'], ['03', 'Buddha'], ['04', 'Kristian'], ['05', 'Lain-lain'], ]; const races = [ ['M', 'Melayu'], ['C', 'Cina'], ['I', 'India'], ['L', 'Lain-lain'], ]; let disablePointerEventsOnClickableInputs = () => { modal_form.querySelector("input[name='bumiputera']").style.pointerEvents = 'none'; modal_form.querySelector("input[name='warganegara']").style.pointerEvents = 'none'; modal_form.querySelector("select[name='bangsa']").style.pointerEvents = 'none'; modal_form.querySelector("select[name='agama']").style.pointerEvents = 'none'; }; let mykad_modal = document.querySelector("#daftar_pelanggan_mykad_reader"); let photo_container = mykad_modal.querySelector("#photo-mykad-container"); let photo = mykad_modal.querySelector("#photo-mykad"); modal_form.querySelector("textarea[name='name']").value = data.name || data.gmpc_name || ''; modal_form.querySelector("input[name='noic']").value = getMyKadNumberWithoutHyphens(data.nric); modal_form.querySelector("input[name='tarikh_lahir']").value = formatDateOfBirth(data.dob); modal_form.querySelector("input[name='negeri_lahir']").value = data.birth_place || ''; modal_form.querySelector("input[name='jantina']").value = mapGender(data.gender); modal_form.querySelector("input[name='warganegara']").checked = String(data.citizenship || '').toUpperCase() === 'WARGANEGARA'; modal_form.querySelector("input[name='bumiputera']").checked = getBumiputeraStatus(data.race); modal_form.querySelector("select[name='agama']").value = getCodeFromValue(data.religion, religions, '05'); modal_form.querySelector("textarea[name='alamat']").value = joinAddress(data); modal_form.querySelector("input[name='poskod']").value = data.postcode || ''; modal_form.querySelector("input[name='daerah']").value = data.city || ''; modal_form.querySelector("input[name='negeri']").value = data.state || ''; modal_form.querySelector("input[name='age']").value = data.age != null && data.age !== '' ? data.age : getAgeFromDateOfBirth(data.dob); modal_form.querySelector("select[name='bangsa']").value = getCodeFromValue(data.race, races, 'L'); if (data.has_photo && data.photo_base64) { let mime = data.photo_mime || 'image/jpeg'; photo.src = 'data:' + mime + ';base64,' + data.photo_base64; photo_container.classList.remove('d-none'); photo_container.classList.add('d-flex'); } else { photo.src = original_photo_loader; photo_container.classList.add('d-none'); photo_container.classList.remove('d-flex'); } disablePointerEventsOnClickableInputs(); $(modal_form.querySelector("input[name='poskod']")).trigger('change'); }; let hideMykadPhoto = () => { let photo = document.getElementById("photo-mykad"); let photo_container = document.getElementById("photo-mykad-container"); if (photo) { photo.src = original_photo_loader; } if (photo_container) { photo_container.classList.add('d-none'); photo_container.classList.remove('d-flex'); } }; if (button_read_mykad) { button_read_mykad.addEventListener("click", handleMyKadReader); } if (button_close_modal) { button_close_modal.addEventListener("click", hideMykadPhoto); }