// --- Helper Fungsi --- /** * Normalisasi nomor telepon ke format standard e.g. 628... * @param {string} phone - Nomor telepon mentah * @returns {string} Nomor telepon ternormalisasi */ function normalizePhone(phone) { let digits = String(phone || '').replace(/\D/g, ''); if (digits.startsWith('0')) { digits = `62${digits.slice(1)}`; } else if (digits.startsWith('8')) { digits = `62${digits}`; } return digits; } /** * Format nomor telepon untuk tampilan UI * @param {string} phone - Nomor telepon mentah * @returns {string} Nomor telepon terformat (e.g. +62 8xx-xxxx-xxxx) */ function formatPhoneForDisplay(phone) { const norm = normalizePhone(phone); if (!norm) return '-'; return `+${norm.slice(0, 2)} ${norm.slice(2, 5)}-${norm.slice(5, 9)}-${norm.slice(9)}`; } /** * Memvalidasi apakah nomor telepon valid untuk digunakan di WhatsApp. * @param {string} phone - Nomor telepon mentah * @returns {boolean} True jika nomor valid (seluler Indonesia 10-15 digit) */ function isValidWhatsAppNumber(phone) { const norm = normalizePhone(phone); return norm.startsWith('628') && norm.length >= 10 && norm.length <= 15; } /** * Merender daftar staf pendukung ke dalam container HTML. * @param {Array} staffs - Array data staf pendukung */ export function renderStaffCards(staffs) { const container = document.getElementById('staff-container'); if (!container) return; container.innerHTML = ''; if (!staffs || staffs.length === 0) { container.innerHTML = `

Data staf pendukung tidak tersedia saat ini.

`; return; } staffs.forEach((staff, index) => { const { name, role, image, location, phone, schedule, tasks } = staff; const scheduleHTML = schedule.map(sched => `
` + `${sched.day}` + `${sched.time} | ${sched.break}` + `
` ).join(''); const tasksHTML = tasks.map(task => `
  • ${task}
  • `).join(''); const initial = name ? name.charAt(0).toUpperCase() : ''; const card = document.createElement('div'); card.className = `staff-card reveal reveal-delay-${index + 1}`; card.innerHTML = `
    ${initial} ${name}
    ${name}
    ${role}
    📍
    Lokasi${location}
    🕒
    Jam Layanan
    ${scheduleHTML}
    📞
    Kontak${phone}
    Tugas & Tanggung Jawab
    `; container.appendChild(card); }); } // ============================================================ // --- Render SOP sebagai Card Grid dengan Search & Filter --- // ============================================================ const CATEGORY_COLORS = { akademik: 'sop-cat-akademik', praktikum: 'sop-cat-praktikum', }; /** * Ambil label kategori * @param {string} category - Kategori SOP ('akademik' atau 'praktikum') * @returns {string} Label kategori */ function getCatLabel(category) { const map = { akademik: 'Akademik', praktikum: 'Praktikum' }; return map[category] || 'Akademik'; } /** * Buat HTML untuk satu SOP card di grid. * @param {Object} sop - Data objek SOP * @returns {string} String markup HTML kartu SOP */ function buildSopCard(sop) { const { tab_id: tabId, category, icon = 'fa-file-alt', title, description, procedures } = sop; const catLabel = getCatLabel(category); const catClass = CATEGORY_COLORS[category] || ''; return `

    ${title}

    ${catLabel}

    ${description}

    `; } /** * Buat HTML untuk isi modal detail SOP. * @param {Object} sop - Data objek SOP * @returns {string} String markup HTML isi modal */ function buildSopModalContent(sop) { const { category, icon = 'fa-file-alt', title, description, purpose, requirements, procedures, staff_responsible: staffResponsible, additional_info: additionalInfo } = sop; const catLabel = getCatLabel(category); const catClass = CATEGORY_COLORS[category] || ''; const reqsHTML = requirements.map(req => `
  • ${req}
  • `).join(''); const procsHTML = procedures.map(proc => `
  • ${proc}
  • `).join(''); // staffResponsible bersifat opsional — sembunyikan blok jika tidak ada const hasStaff = Array.isArray(staffResponsible) && staffResponsible.length > 0; const staffHTML = hasStaff ? staffResponsible.map(staffName => `${staffName}`).join('') : ''; const staffBlockHTML = hasStaff ? `
    Staf Bertanggung Jawab (Bersifat Opsional)
    ${staffHTML}
    ` : ''; // additionalInfo bersifat opsional — sembunyikan blok jika tidak ada const additionalInfoHTML = additionalInfo ? `
    â„šī¸

    Informasi Tambahan (Bersifat Opsional): ${additionalInfo}

    ` : ''; return `
    ${catLabel}

    ${title}

    ${description}

    Tujuan

    ${purpose}

    Persyaratan
      ${reqsHTML}
    Alur Prosedur
      ${procsHTML}
    ${staffBlockHTML} ${additionalInfoHTML}
    `; } /** * Render semua SOP card ke grid. * @param {Array} sops - Array objek SOP */ export function renderSopCards(sops) { const container = document.getElementById('sop-cards-container'); if (!container) return; // simpan data global di element agar bisa diakses filter/search container._sopData = sops; // Tampilkan pesan jika data SOP kosong atau gagal dimuat if (!sops || sops.length === 0) { container.innerHTML = `

    Data SOP tidak tersedia saat ini.

    `; updateResultInfo(0, 0); return; } container.innerHTML = sops.map(buildSopCard).join(''); updateResultInfo(sops.length, sops.length); } /** Update teks info jumlah hasil */ function updateResultInfo(shown, total) { const el = document.getElementById('sop-result-info'); if (!el) return; if (shown === total) { el.textContent = `Menampilkan ${total} SOP`; } else { el.textContent = `Menampilkan ${shown} dari ${total} SOP`; } } /** * Memfilter dan menyaring kartu SOP berdasarkan query pencarian dan chip filter aktif. */ function applySopFilter() { const container = document.getElementById('sop-cards-container'); if (!container) return; const query = (document.getElementById('sop-search-input')?.value || '').toLowerCase().trim(); const activeChip = document.querySelector('.sop-chip.active'); const activeFilter = activeChip ? activeChip.dataset.filter : 'semua'; const cards = container.querySelectorAll('.sop-grid-card'); let visibleCount = 0; cards.forEach(card => { const title = card.dataset.title || ''; const code = card.dataset.code || ''; const category = card.dataset.category || ''; const matchesSearch = !query || title.includes(query) || code.includes(query); const matchesFilter = activeFilter === 'semua' || category === activeFilter; const isVisible = matchesSearch && matchesFilter; card.style.display = isVisible ? '' : 'none'; if (isVisible) visibleCount++; }); const emptyState = document.getElementById('sop-empty-state'); if (emptyState) emptyState.style.display = visibleCount === 0 ? 'flex' : 'none'; updateResultInfo(visibleCount, cards.length); } /** * Membuka modal detail untuk SOP tertentu berdasarkan tab_id. * @param {string} tabId - ID unik tab/SOP yang ingin ditampilkan */ function openSopModal(tabId) { const container = document.getElementById('sop-cards-container'); if (!container || !container._sopData) return; const sop = container._sopData.find(item => item.tab_id === tabId); if (!sop) return; const modalContent = document.getElementById('sop-modal-content'); const modalOverlay = document.getElementById('sop-modal-overlay'); if (!modalContent || !modalOverlay) return; modalContent.innerHTML = buildSopModalContent(sop); modalOverlay.classList.add('active'); document.body.style.overflow = 'hidden'; // Gulir isi modal ke paling atas const modalBox = document.getElementById('sop-modal-box'); if (modalBox) modalBox.scrollTop = 0; } /** * Menutup modal detail SOP yang sedang aktif. */ function closeSopModal() { const modalOverlay = document.getElementById('sop-modal-overlay'); if (modalOverlay) modalOverlay.classList.remove('active'); document.body.style.overflow = ''; } /** * Menginisialisasi interaksi halaman SOP (pencarian, filter chip, modal detail). */ export function initSopInteraction() { // Search input const searchInput = document.getElementById('sop-search-input'); const searchClear = document.getElementById('sop-search-clear'); if (searchInput) { searchInput.addEventListener('input', () => { if (searchClear) { searchClear.style.display = searchInput.value ? 'flex' : 'none'; } applySopFilter(); }); } if (searchClear) { searchClear.addEventListener('click', () => { if (searchInput) { searchInput.value = ''; } searchClear.style.display = 'none'; applySopFilter(); }); } // Filter chips document.querySelectorAll('.sop-chip').forEach(chip => { chip.addEventListener('click', () => { document.querySelectorAll('.sop-chip').forEach(activeChip => { activeChip.classList.remove('active'); }); chip.classList.add('active'); applySopFilter(); }); }); // Empty state reset button const emptyReset = document.getElementById('sop-empty-reset'); if (emptyReset) { emptyReset.addEventListener('click', () => { if (searchInput) { searchInput.value = ''; } if (searchClear) { searchClear.style.display = 'none'; } document.querySelectorAll('.sop-chip').forEach(activeChip => { activeChip.classList.remove('active'); }); const allChip = document.querySelector('[data-filter="semua"]'); if (allChip) { allChip.classList.add('active'); } applySopFilter(); }); } // Card detail button (event delegation) const container = document.getElementById('sop-cards-container'); if (container) { container.addEventListener('click', (event) => { const btn = event.target.closest('.sgc-detail-btn'); if (btn) { openSopModal(btn.dataset.tabId); } }); } // Modal close button const closeBtn = document.getElementById('sop-modal-close'); if (closeBtn) { closeBtn.addEventListener('click', closeSopModal); } // Close on overlay click const overlay = document.getElementById('sop-modal-overlay'); if (overlay) { overlay.addEventListener('click', (event) => { if (event.target === overlay) { closeSopModal(); } }); } // Close on Escape key document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { closeSopModal(); } }); } // --- Kompatibilitas Mundur --- /** * Stub kompatibilitas mundur untuk renderSopTabs. * @param {Array} sops - Array objek SOP */ export function renderSopTabs(sops) { renderSopCards(sops); } /** * Stub kompatibilitas mundur untuk initSopTabsInteraction. */ export function initSopTabsInteraction() { initSopInteraction(); } // --- Interaksi & Animasi --- /** * Menginisialisasi animasi scroll reveal dan counter statistik pada hero section. */ export function initAnimations() { // Scroll Reveal const revealObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; entry.target.classList.add('visible'); revealObserver.unobserve(entry.target); }); }, { threshold: 0.1 }); document.querySelectorAll('.reveal').forEach((el) => revealObserver.observe(el)); // Hero Counter const COUNT_ANIMATION_DURATION_MS = 800; /** * Menjalankan animasi hitung angka (counter) * @param {Element} targetElement - Elemen target angka * @param {number} targetValue - Nilai akhir counter */ function animateCount(targetElement, targetValue) { if (!targetElement) return; const stepValue = targetValue / (COUNT_ANIMATION_DURATION_MS / 16); let currentValue = 0; const timerId = setInterval(() => { currentValue += stepValue; if (currentValue >= targetValue) { currentValue = targetValue; clearInterval(timerId); } targetElement.textContent = Math.floor(currentValue).toString(); }, 16); } const staffCounter = document.getElementById('stat-staf'); const sopCounter = document.getElementById('stat-sop'); // Hitung jumlah data yang berhasil di-render (tidak termasuk template) const totalStaffCards = document.querySelectorAll('.staff-card').length; const totalSopCards = document.querySelectorAll('.sop-grid-card').length; animateCount(staffCounter, totalStaffCards); animateCount(sopCounter, totalSopCards); } // --- FAQ Accordion --- /** * Membuka item FAQ yang diklik; menutup item lain yang sebelumnya * terbuka (perilaku accordion satu-terbuka-sekaligus). * @param {Element} questionEl - Elemen .faq-question yang diklik */ function toggleFaq(questionEl) { const item = questionEl.closest('.faq-item'); if (!item) return; const isOpen = item.classList.contains('open'); // Tutup semua item terlebih dahulu document.querySelectorAll('.faq-item').forEach(faqItem => { faqItem.classList.remove('open'); }); // Buka item yang diklik hanya jika sebelumnya tertutup if (!isOpen) { item.classList.add('open'); } } /** * Menginisialisasi event listener klik untuk setiap accordion FAQ. */ export function initFaq() { document.querySelectorAll('.faq-question').forEach(questionEl => { questionEl.addEventListener('click', () => toggleFaq(questionEl)); }); } // --- Render FAQ --- /** * Merender section FAQ secara dinamis dari data-faq.json. * Diinjeksikan ke dalam #faq-container di index.html. * @param {Array} faqs - Array objek FAQ dari data-faq.json */ export function renderFaqSection(faqs) { const container = document.getElementById('faq-container'); if (!container) return; if (!faqs || faqs.length === 0) { container.innerHTML = `

    Data FAQ tidak tersedia saat ini.

    `; return; } const delayClasses = ['reveal-delay-1', 'reveal-delay-2', 'reveal-delay-3']; const itemsHTML = faqs.map((faq, index) => { const delayClass = delayClasses[index] || ''; return `
    ${faq.question}
    +
    ${faq.answer}
    `; }).join(''); container.innerHTML = itemsHTML; }