forked from izu/student-web-if-development-kit
merge(c4a): combine bilingual and UI fix commits into feature branch
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
# Panduan Pengelolaan Data Kelompok C4A
|
||||
|
||||
Dokumentasi ini dibuat untuk memudahkan pengembang atau pengguna berikutnya dalam mengelola data Staf Pendukung, SOP, dan FAQ pada halaman C4A.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Aturan Umum Pengelolaan Data
|
||||
|
||||
1. **Lokasi Berkas Data**:
|
||||
Seluruh data disimpan dalam format JSON di direktori `groups/C4A/data/`:
|
||||
- Data Staf: `groups/C4A/data/data-staff.json`
|
||||
- Data SOP: `groups/C4A/data/data-sop.json`
|
||||
- Data FAQ: `groups/C4A/data/data-faq.json`
|
||||
|
||||
2. **Dukungan Bilingual (ID/EN)**:
|
||||
Semua teks konten yang ditampilkan ke pengguna mendukung dua bahasa. Anda perlu menggunakan objek dengan properti `id` (Bahasa Indonesia) dan `en` (Bahasa Inggris) pada setiap field teks. Jika properti `en` tidak diisi atau data masih berupa *string* biasa, sistem akan otomatis menggunakan Bahasa Indonesia sebagai *fallback*.
|
||||
|
||||
3. **Ikon FontAwesome (v4)**:
|
||||
Jika ingin mengubah atau menambahkan logo/ikon pada data SOP, harap gunakan daftar ikon dari **FontAwesome v4** melalui tautan berikut:
|
||||
👉 **[FontAwesome v4 Icons](https://fontawesome.com/v4/icons/)** (Contoh penggunaan: `fa-dot-circle-o`, `fa-graduation-cap`, `fa-flask`).
|
||||
|
||||
4. **Field Opsional**:
|
||||
Pada data SOP, properti **`staff_responsible`** (Staf Bertanggung Jawab) dan **`additional_info`** (Informasi Tambahan) bersifat **opsional**. Jika informasi tersebut tidak tersedia, kosongkan nilainya atau hapus propertinya dari JSON agar UI secara otomatis menyembunyikan bloknya.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Template Penambahan Data
|
||||
|
||||
Silakan salin-tempel (copy-paste) format di bawah ini ke dalam array JSON yang sesuai di folder `data/` untuk menambahkan data baru.
|
||||
|
||||
### 1. Template Data Staf (`data-staff.json`)
|
||||
```json
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Nama Staf Beserta Gelar",
|
||||
"role": {
|
||||
"id": "Peran / Jabatan Staf",
|
||||
"en": "Staff Role / Title"
|
||||
},
|
||||
"image": "assets/images/nama-file-foto.png",
|
||||
"location": {
|
||||
"id": "Lokasi Ruang Kerja / Kantor Staf",
|
||||
"en": "Staff Workspace / Office Location"
|
||||
},
|
||||
"contact_consent": true,
|
||||
"phone": "628xxxxxxxxxx",
|
||||
"schedule": [
|
||||
{
|
||||
"day": {
|
||||
"id": "Hari Kerja (contoh: Senin – Jumat)",
|
||||
"en": "Work Days (e.g., Monday – Friday)"
|
||||
},
|
||||
"time": "07.30–16.00 WIB",
|
||||
"break": {
|
||||
"id": "Jam Istirahat (contoh: istirahat 12.00–13.00 WIB)",
|
||||
"en": "Break Time (e.g., break 12.00–13.00 WIB)"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"id": "Tugas dan tanggung jawab utama 1",
|
||||
"en": "Main task and responsibility 1"
|
||||
},
|
||||
{
|
||||
"id": "Tugas dan tanggung jawab utama 2",
|
||||
"en": "Main task and responsibility 2"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
*Catatan: Letakkan file gambar/foto staf ke dalam direktori `groups/C4A/assets/images/` dan masukkan nama berkas fotonya pada properti `image` di atas. Untuk nomor telepon (`phone`), Anda dapat menggunakan format `08xxxxxxxxxx` atau `628xxxxxxxxxx` karena skrip JavaScript akan melakukan normalisasi secara otomatis.*
|
||||
|
||||
### 2. Template Data SOP (`data-sop.json`)
|
||||
```json
|
||||
{
|
||||
"tab_id": "id-unik-sop-tanpa-spasi",
|
||||
"tab_number": 9,
|
||||
"category": "akademik",
|
||||
"icon": "fa-file-text",
|
||||
"title": {
|
||||
"id": "Judul Lengkap SOP",
|
||||
"en": "Full SOP Title"
|
||||
},
|
||||
"description": {
|
||||
"id": "Deskripsi singkat mengenai SOP ini.",
|
||||
"en": "Brief description of this SOP."
|
||||
},
|
||||
"purpose": {
|
||||
"id": "Tujuan dibuatnya standar operasional prosedur ini.",
|
||||
"en": "The purpose of creating this standard operating procedure."
|
||||
},
|
||||
"requirements": [
|
||||
{ "id": "Persyaratan administratif/akademik 1", "en": "Administrative/academic requirement 1" },
|
||||
{ "id": "Persyaratan administratif/akademik 2", "en": "Administrative/academic requirement 2" }
|
||||
],
|
||||
"procedures": [
|
||||
{ "id": "Langkah alur prosedur ke-1", "en": "Procedure flow step 1" },
|
||||
{ "id": "Langkah alur prosedur ke-2", "en": "Procedure flow step 2" }
|
||||
],
|
||||
"staff_responsible": [
|
||||
"👤 Staf Administrasi Informatika / Informatics Administrative Staff"
|
||||
],
|
||||
"additional_info": {
|
||||
"id": "Informasi tambahan penting.",
|
||||
"en": "Important additional information."
|
||||
}
|
||||
}
|
||||
```
|
||||
*Catatan: Properti `category` bernilai antara `"akademik"` atau `"praktikum"`. Properti `staff_responsible` dan `additional_info` bersifat opsional.*
|
||||
|
||||
### 3. Template Data FAQ (`data-faq.json`)
|
||||
```json
|
||||
{
|
||||
"id": 11,
|
||||
"question": {
|
||||
"id": "Pertanyaan yang sering diajukan?",
|
||||
"en": "Frequently asked question?"
|
||||
},
|
||||
"answer": {
|
||||
"id": "Jawaban dari pertanyaan tersebut. Boleh menggunakan tag HTML seperti <strong>tebal</strong>.",
|
||||
"en": "Answer to the question. HTML tags like <strong>bold</strong> can be used."
|
||||
}
|
||||
}
|
||||
```
|
||||
+23
-21
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id" class="scroll-smooth">
|
||||
|
||||
<head>
|
||||
@@ -1160,13 +1160,13 @@
|
||||
<!-- HERO SECTION -->
|
||||
<section class="c4a-hero reveal">
|
||||
<div class="c4a-hero-inner">
|
||||
<div class="c4a-hero-tag">LAYANAN AKADEMIK • INFORMATIKA UNTAN
|
||||
<div class="c4a-hero-tag" data-i18n="grp_c4a_hero_tag">LAYANAN AKADEMIK • INFORMATIKA UNTAN
|
||||
</div>
|
||||
<h1 class="c4a-hero-title">
|
||||
Staf Pendukung & <br />
|
||||
<h1 class="c4a-hero-title" data-i18n-html="grp_c4a_hero_title">
|
||||
Staf Pendukung & <br/>
|
||||
<span class="highlight-orange">Standar Operasional Prosedur (SOP)</span>
|
||||
</h1>
|
||||
<p class="c4a-hero-desc">Halaman ini menampilkan informasi petugas
|
||||
<p class="c4a-hero-desc" data-i18n="grp_c4a_hero_desc">Halaman ini menampilkan informasi petugas
|
||||
layanan akademik serta
|
||||
prosedur administrasi
|
||||
mahasiswa
|
||||
@@ -1174,12 +1174,12 @@
|
||||
<div class="hero-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="stat-staf">0</div>
|
||||
<div class="stat-label">Staf Aktif</div>
|
||||
<div class="stat-label" data-i18n="grp_c4a_stat_staf">Staf Aktif</div>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="stat-sop">0</div>
|
||||
<div class="stat-label">Jenis SOP</div>
|
||||
<div class="stat-label" data-i18n="grp_c4a_stat_sop">Jenis SOP</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1188,10 +1188,10 @@
|
||||
<!-- STAFF SECTION -->
|
||||
<section class="content-section">
|
||||
<div class="section-header">
|
||||
<div class="section-tag"><span class="dot"></span> <span>Tim
|
||||
<div class="section-tag"><span class="dot"></span> <span data-i18n="grp_c4a_staff_tag">Tim
|
||||
Kami</span></div>
|
||||
<h2 class="section-title">Staf Pendukung</h2>
|
||||
<p class="section-subtitle">Staf yang siap melayani kebutuhan
|
||||
<h2 class="section-title" data-i18n="grp_c4a_staff_title">Staf Pendukung</h2>
|
||||
<p class="section-subtitle" data-i18n="grp_c4a_staff_subtitle">Staf yang siap melayani kebutuhan
|
||||
akademik dan operasional
|
||||
laboratorium di Prodi Informatika UNTAN.</p>
|
||||
</div>
|
||||
@@ -1203,10 +1203,10 @@
|
||||
<!-- SOP SECTION -->
|
||||
<section class="sop-section">
|
||||
<div class="section-header">
|
||||
<div class="section-tag"><span class="dot"></span> <span>Prosedur
|
||||
<div class="section-tag"><span class="dot"></span> <span data-i18n="grp_c4a_sop_tag">Prosedur
|
||||
Resmi</span></div>
|
||||
<h2 class="section-title">Standar Operasional Prosedur (SOP)</h2>
|
||||
<p class="section-subtitle">Standar Operasional Prosedur untuk
|
||||
<h2 class="section-title" data-i18n="grp_c4a_sop_title">Standar Operasional Prosedur (SOP)</h2>
|
||||
<p class="section-subtitle" data-i18n="grp_c4a_sop_subtitle">Standar Operasional Prosedur untuk
|
||||
berbagai kebutuhan administrasi
|
||||
akademik mahasiswa. Gunakan pencarian atau filter untuk menemukan SOP yang Anda butuhkan.
|
||||
</p>
|
||||
@@ -1217,6 +1217,7 @@
|
||||
<div class="sop-search-wrap">
|
||||
<i class="fas fa-search sop-search-icon"></i>
|
||||
<input type="search" id="sop-search-input" class="sop-search-input"
|
||||
data-i18n-placeholder="grp_c4a_sop_search_placeholder"
|
||||
placeholder="Cari SOP berdasarkan judul..." autocomplete="off" aria-label="Cari SOP" />
|
||||
<button class="sop-search-clear" id="sop-search-clear" aria-label="Hapus pencarian"
|
||||
style="display:none;">
|
||||
@@ -1225,12 +1226,12 @@
|
||||
</div>
|
||||
<div class="sop-filter-chips" id="sop-filter-chips" role="group"
|
||||
aria-label="Filter kategori SOP">
|
||||
<button class="sop-chip active" data-filter="semua" id="chip-semua">Semua</button>
|
||||
<button class="sop-chip active" data-filter="semua" id="chip-semua" data-i18n="grp_c4a_sop_chip_semua">Semua</button>
|
||||
<button class="sop-chip" data-filter="akademik" id="chip-akademik">
|
||||
<i class="fas fa-graduation-cap"></i> <span>Akademik</span>
|
||||
<i class="fas fa-graduation-cap"></i> <span data-i18n="grp_c4a_sop_chip_akademik">Akademik</span>
|
||||
</button>
|
||||
<button class="sop-chip" data-filter="praktikum" id="chip-praktikum">
|
||||
<i class="fas fa-flask"></i> <span>Praktikum</span>
|
||||
<i class="fas fa-flask"></i> <span data-i18n="grp_c4a_sop_chip_praktikum">Praktikum</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1244,8 +1245,8 @@
|
||||
<!-- EMPTY STATE -->
|
||||
<div class="sop-empty-state" id="sop-empty-state" style="display:none;">
|
||||
<i class="fas fa-search-minus sop-empty-icon"></i>
|
||||
<p class="sop-empty-title">SOP tidak ditemukan</p>
|
||||
<p class="sop-empty-desc">Coba kata kunci lain atau reset
|
||||
<p class="sop-empty-title" data-i18n="grp_c4a_sop_empty_title">SOP tidak ditemukan</p>
|
||||
<p class="sop-empty-desc" data-i18n="grp_c4a_sop_empty_desc">Coba kata kunci lain atau reset
|
||||
filter pencarian.</p>
|
||||
<button class="sop-empty-reset" id="sop-empty-reset">
|
||||
<i class="fas fa-undo"></i> <span data-i18n="grp_c4a_sop_reset_btn">Reset Pencarian</span>
|
||||
@@ -1268,12 +1269,13 @@
|
||||
<section class="faq-section-wrap">
|
||||
<div class="faq-section-inner">
|
||||
<div class="faq-header reveal">
|
||||
<div class="section-tag"><span class="dot"></span> <span>Pertanyaan Umum</span></div>
|
||||
<h2>Pertanyaan yang Sering Diajukan</h2>
|
||||
<p>Temukan jawaban cepat atas pertanyaan umum mahasiswa
|
||||
<div class="section-tag"><span class="dot"></span> <span data-i18n="grp_c4a_faq_tag">Pertanyaan Umum</span></div>
|
||||
<h2 data-i18n="grp_c4a_faq_title">Pertanyaan yang Sering Diajukan</h2>
|
||||
<p data-i18n="grp_c4a_faq_subtitle">Temukan jawaban cepat atas pertanyaan umum mahasiswa
|
||||
seputar layanan akademik prodi.</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="faq-wrap" id="faq-container">
|
||||
<!-- Item FAQ diisi secara dinamis melalui main.js berdasarkan data-faq.json -->
|
||||
</div>
|
||||
|
||||
@@ -52,3 +52,5 @@ export async function getFaqData() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+19
-4
@@ -10,12 +10,14 @@ import {
|
||||
renderFaqSection,
|
||||
initSopInteraction,
|
||||
initAnimations,
|
||||
initFaq
|
||||
initFaq,
|
||||
applySopFilter
|
||||
} from './ui.js';
|
||||
|
||||
// Cache data secara global untuk mendukung rendering UI
|
||||
let cachedStaffData = [];
|
||||
let cachedSopData = [];
|
||||
let cachedFaqData = [];
|
||||
|
||||
/**
|
||||
* Menginisialisasi aplikasi dengan mengambil seluruh data secara pararel,
|
||||
@@ -32,10 +34,11 @@ async function initApp() {
|
||||
|
||||
cachedStaffData = staffData || [];
|
||||
cachedSopData = sopData || [];
|
||||
cachedFaqData = faqData || [];
|
||||
|
||||
renderStaffCards(cachedStaffData);
|
||||
renderSopCards(cachedSopData);
|
||||
renderFaqSection(faqData || []);
|
||||
renderFaqSection(cachedFaqData);
|
||||
|
||||
initSopInteraction();
|
||||
initFaq();
|
||||
@@ -49,8 +52,20 @@ async function initApp() {
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Fitur bilingual sudah dihapus - languageChanged listener tidak lagi diperlukan
|
||||
// Silakan hapus atau archive jika ada implementasi global untuk bahasa
|
||||
// Re-render UI saat bahasa berubah (data tetap sama, hanya label UI yang berganti via t())
|
||||
document.addEventListener('languageChanged', () => {
|
||||
renderStaffCards(cachedStaffData);
|
||||
renderSopCards(cachedSopData);
|
||||
renderFaqSection(cachedFaqData);
|
||||
initFaq();
|
||||
applySopFilter();
|
||||
|
||||
// Re-inisialisasi observer agar elemen .reveal yang baru di-render mendapat class .visible
|
||||
setTimeout(() => {
|
||||
initAnimations();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Jalankan inisialisasi aplikasi saat DOM siap
|
||||
document.addEventListener('DOMContentLoaded', initApp);
|
||||
|
||||
|
||||
+131
-38
@@ -1,5 +1,31 @@
|
||||
// --- Helper Fungsi ---
|
||||
|
||||
/**
|
||||
* Helper untuk menerjemahkan string berdasarkan key dari localization.js.
|
||||
* Menggunakan fallback jika key tidak ditemukan atau localization.js tidak aktif.
|
||||
*/
|
||||
function t(key, fallback) {
|
||||
if (window.t) {
|
||||
const val = window.t(key);
|
||||
if (val !== key) return val;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper untuk melokalisasi teks dari objek bilingual {id, en} atau string biasa.
|
||||
* @param {string|Object} field - Data teks (bisa string biasa atau objek {id, en})
|
||||
* @returns {string} Teks terlokalisasi
|
||||
*/
|
||||
function loc(field) {
|
||||
if (!field) return '';
|
||||
if (typeof field === 'object') {
|
||||
const lang = (window.getLang && typeof window.getLang === 'function') ? window.getLang() : 'id';
|
||||
return field[lang] || field['id'] || '';
|
||||
}
|
||||
return String(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalisasi nomor telepon ke format standard e.g. 628...
|
||||
* @param {string} phone - Nomor telepon mentah
|
||||
@@ -36,6 +62,29 @@ function isValidWhatsAppNumber(phone) {
|
||||
return norm.startsWith('628') && norm.length >= 10 && norm.length <= 15;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enkripsi nomor telepon menggunakan Base64 untuk keamanan kontak.
|
||||
* @param {string} phone - Nomor telepon ternormalisasi
|
||||
* @returns {string} Nomor telepon terenkripsi Base64
|
||||
*/
|
||||
function encodePhone(phone) {
|
||||
return btoa(normalizePhone(phone));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dekripsi nomor telepon dari Base64.
|
||||
* @param {string} encoded - Nomor telepon terenkripsi Base64
|
||||
* @returns {string} Nomor telepon terdekripsi
|
||||
*/
|
||||
function decodePhone(encoded) {
|
||||
try {
|
||||
return atob(encoded);
|
||||
} catch (error) {
|
||||
console.error('Gagal mendekripsi nomor telepon:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merender daftar staf pendukung ke dalam container HTML.
|
||||
* @param {Array<Object>} staffs - Array data staf pendukung
|
||||
@@ -47,7 +96,7 @@ export function renderStaffCards(staffs) {
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!staffs || staffs.length === 0) {
|
||||
container.innerHTML = `<p class="empty-state-msg">Data staf pendukung tidak tersedia saat ini.</p>`;
|
||||
container.innerHTML = `<p class="empty-state-msg">${t('grp_c4a_empty_staff', 'Data staf pendukung tidak tersedia saat ini.')}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,18 +107,24 @@ export function renderStaffCards(staffs) {
|
||||
image,
|
||||
location,
|
||||
phone,
|
||||
contact_consent: contactConsent,
|
||||
schedule,
|
||||
tasks
|
||||
} = staff;
|
||||
|
||||
const scheduleHTML = schedule.map(sched =>
|
||||
`<div class="schedule-item">` +
|
||||
`<span class="s-day">${sched.day}</span>` +
|
||||
`<span class="s-time">${sched.time} <span class="s-break">| ${sched.break}</span></span>` +
|
||||
`<span class="s-day">${loc(sched.day)}</span>` +
|
||||
`<span class="s-time">${loc(sched.time)} <span class="s-break">| ${loc(sched.break)}</span></span>` +
|
||||
`</div>`
|
||||
).join('');
|
||||
|
||||
const tasksHTML = tasks.map(task => `<li>${task}</li>`).join('');
|
||||
const tasksHTML = tasks.map(task => `<li>${loc(task)}</li>`).join('');
|
||||
const isPhoneValid = isValidWhatsAppNumber(phone);
|
||||
const encodedPhone = isPhoneValid ? encodePhone(phone) : '';
|
||||
const displayPhone = isPhoneValid ? formatPhoneForDisplay(phone) : '-';
|
||||
const hideContactItemClass = contactConsent ? '' : 'is-hidden';
|
||||
const hideContactActionClass = (contactConsent && isPhoneValid) ? '' : 'is-hidden';
|
||||
const initial = name ? name.charAt(0).toUpperCase() : '';
|
||||
|
||||
const card = document.createElement('div');
|
||||
@@ -82,29 +137,44 @@ export function renderStaffCards(staffs) {
|
||||
<img src="${image}" alt="${name}" onerror="this.style.display='none'"/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="staff-name">${name}</div>
|
||||
<div class="staff-role">${role}</div>
|
||||
<div class="staff-name">${loc(name)}</div>
|
||||
<div class="staff-role">${loc(role)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="staff-card-body">
|
||||
<div class="staff-info-grid">
|
||||
<div class="staff-info-item"><span class="icon">📍</span><div><span class="label">Lokasi</span><span class="val">${location}</span></div></div>
|
||||
<div class="staff-info-item"><span class="icon">📍</span><div><span class="label">${t('grp_c4a_label_location', 'Lokasi')}</span><span class="val">${loc(location)}</span></div></div>
|
||||
<div class="staff-info-item">
|
||||
<span class="icon">🕒</span>
|
||||
<div style="flex:1">
|
||||
<span class="label">Jam Layanan</span>
|
||||
<span class="label">${t('grp_c4a_label_service_hours', 'Jam Layanan')}</span>
|
||||
<div class="schedule-list">${scheduleHTML}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="staff-info-item"><span class="icon">📞</span><div><span class="label">Kontak</span><span class="val">${phone}</span></div></div>
|
||||
<div class="staff-info-item staff-contact-item ${hideContactItemClass}"><span class="icon">📞</span><div><span class="label">${t('grp_c4a_label_wa_contact', 'Kontak WhatsApp')}</span><span class="val">${displayPhone}</span></div></div>
|
||||
</div>
|
||||
<div class="staff-divider"></div>
|
||||
<div class="staff-tasks-title">Tugas & Tanggung Jawab</div>
|
||||
<div class="staff-tasks-title">${t('grp_c4a_label_tasks', 'Tugas & Tanggung Jawab')}</div>
|
||||
<ul class="staff-tasks">${tasksHTML}</ul>
|
||||
</div>
|
||||
<div class="staff-card-actions staff-contact-actions ${hideContactActionClass}">
|
||||
<button class="btn-staff btn-staff-wa" data-wa="${encodedPhone}" aria-label="${t('grp_c4a_label_send_wa', 'Kirim Pesan WA')} ${name}"><svg class="wa-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/></svg> ${t('grp_c4a_label_send_wa', 'Kirim Pesan WA')}</button>
|
||||
</div>
|
||||
|
||||
`;
|
||||
|
||||
container.appendChild(card);
|
||||
|
||||
// Setup WA button click handler dengan dekrips nomor
|
||||
const waBtn = card.querySelector('[data-wa]');
|
||||
if (waBtn) {
|
||||
waBtn.addEventListener('click', () => {
|
||||
const phoneNum = decodePhone(waBtn.dataset.wa);
|
||||
if (phoneNum) {
|
||||
window.open(`https://wa.me/${phoneNum}`, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// ============================================================
|
||||
@@ -122,8 +192,11 @@ const CATEGORY_COLORS = {
|
||||
* @returns {string} Label kategori
|
||||
*/
|
||||
function getCatLabel(category) {
|
||||
const map = { akademik: 'Akademik', praktikum: 'Praktikum' };
|
||||
return map[category] || 'Akademik';
|
||||
const map = {
|
||||
akademik: t('grp_c4a_sop_chip_akademik', 'Akademik'),
|
||||
praktikum: t('grp_c4a_sop_chip_praktikum', 'Praktikum')
|
||||
};
|
||||
return map[category] || t('grp_c4a_sop_chip_akademik', 'Akademik');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,28 +216,30 @@ function buildSopCard(sop) {
|
||||
|
||||
const catLabel = getCatLabel(category);
|
||||
const catClass = CATEGORY_COLORS[category] || '';
|
||||
const titleText = loc(title);
|
||||
const descText = loc(description);
|
||||
|
||||
return `
|
||||
<article class="sop-grid-card reveal" data-tab-id="${tabId}" data-category="${category}" data-title="${title.toLowerCase()}">
|
||||
<article class="sop-grid-card reveal" data-tab-id="${tabId}" data-category="${category}" data-title="${titleText.toLowerCase()}">
|
||||
<div class="sgc-header">
|
||||
<div class="sgc-icon-wrap">
|
||||
<i class="fas ${icon}"></i>
|
||||
</div>
|
||||
<div class="sgc-meta">
|
||||
<h3 class="sgc-title">${title}</h3>
|
||||
<h3 class="sgc-title">${titleText}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sgc-category-row">
|
||||
<span class="sgc-category ${catClass}">${catLabel}</span>
|
||||
</div>
|
||||
<p class="sgc-desc">${description}</p>
|
||||
<p class="sgc-desc">${descText}</p>
|
||||
<div class="sgc-footer">
|
||||
<div class="sgc-steps">
|
||||
<i class="fas fa-list-ol"></i>
|
||||
<span>${procedures.length} Langkah</span>
|
||||
<span>${procedures.length} ${t('grp_c4a_label_steps', 'Langkah')}</span>
|
||||
</div>
|
||||
<button class="sgc-detail-btn" data-tab-id="${tabId}" aria-label="Lihat Detail ${title}">
|
||||
Lihat Detail <i class="fas fa-arrow-right"></i>
|
||||
<button class="sgc-detail-btn" data-tab-id="${tabId}" aria-label="${t('grp_c4a_label_view_detail', 'Lihat Detail')} ${titleText}">
|
||||
${t('grp_c4a_label_view_detail', 'Lihat Detail')} <i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</article>`;
|
||||
@@ -191,26 +266,31 @@ function buildSopModalContent(sop) {
|
||||
const catLabel = getCatLabel(category);
|
||||
const catClass = CATEGORY_COLORS[category] || '';
|
||||
|
||||
const reqsHTML = requirements.map(req => `<li>${req}</li>`).join('');
|
||||
const procsHTML = procedures.map(proc => `<li>${proc}</li>`).join('');
|
||||
const titleText = loc(title);
|
||||
const descText = loc(description);
|
||||
const purposeText = loc(purpose);
|
||||
const additionalInfoText = loc(additionalInfo);
|
||||
|
||||
const reqsHTML = requirements.map(req => `<li>${loc(req)}</li>`).join('');
|
||||
const procsHTML = procedures.map(proc => `<li>${loc(proc)}</li>`).join('');
|
||||
|
||||
// staffResponsible bersifat opsional — sembunyikan blok jika tidak ada
|
||||
const hasStaff = Array.isArray(staffResponsible) && staffResponsible.length > 0;
|
||||
const staffHTML = hasStaff
|
||||
? staffResponsible.map(staffName => `<span class="sop-staff-pill">${staffName}</span>`).join('')
|
||||
? staffResponsible.map(staffName => `<span class="sop-staff-pill">${loc(staffName)}</span>`).join('')
|
||||
: '';
|
||||
const staffBlockHTML = hasStaff
|
||||
? `<div class="sop-staff-block">
|
||||
<div class="sop-block-label">Staf Bertanggung Jawab (Bersifat Opsional)</div>
|
||||
<div class="sop-block-label">${t('grp_c4a_label_responsible_staff', 'Staf Bertanggung Jawab (Bersifat Opsional)')}</div>
|
||||
<div class="sop-staff-pills">${staffHTML}</div>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
// additionalInfo bersifat opsional — sembunyikan blok jika tidak ada
|
||||
const additionalInfoHTML = additionalInfo
|
||||
const additionalInfoHTML = additionalInfoText
|
||||
? `<div class="sop-info-box">
|
||||
<span class="iico">ℹ️</span>
|
||||
<p class="sop-info-box-text"><strong>Informasi Tambahan (Bersifat Opsional):</strong> ${additionalInfo}</p>
|
||||
<p class="sop-info-box-text"><strong>${t('grp_c4a_label_additional_info', 'Informasi Tambahan (Bersifat Opsional):')}</strong> ${additionalInfoText}</p>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
@@ -224,23 +304,23 @@ function buildSopModalContent(sop) {
|
||||
<div class="sop-modal-badges">
|
||||
<span class="sgc-category ${catClass}">${catLabel}</span>
|
||||
</div>
|
||||
<h2 class="sop-modal-title" id="sop-modal-title">${title}</h2>
|
||||
<p class="sop-modal-desc">${description}</p>
|
||||
<h2 class="sop-modal-title" id="sop-modal-title">${titleText}</h2>
|
||||
<p class="sop-modal-desc">${descText}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sop-modal-body">
|
||||
<div class="sop-block">
|
||||
<div class="sop-block-label">Tujuan</div>
|
||||
<p class="sop-block-text">${purpose}</p>
|
||||
<div class="sop-block-label">${t('grp_c4a_label_purpose', 'Tujuan')}</div>
|
||||
<p class="sop-block-text">${purposeText}</p>
|
||||
</div>
|
||||
<div class="sop-two-col">
|
||||
<div>
|
||||
<div class="sop-block-label">Persyaratan</div>
|
||||
<div class="sop-block-label">${t('grp_c4a_label_requirements', 'Persyaratan')}</div>
|
||||
<ul class="sop-list bullets">${reqsHTML}</ul>
|
||||
</div>
|
||||
<div>
|
||||
<div class="sop-block-label">Alur Prosedur</div>
|
||||
<div class="sop-block-label">${t('grp_c4a_label_procedure', 'Alur Prosedur')}</div>
|
||||
<ul class="sop-list numbered">${procsHTML}</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -260,15 +340,19 @@ export function renderSopCards(sops) {
|
||||
// simpan data global di element agar bisa diakses filter/search
|
||||
container._sopData = sops;
|
||||
|
||||
const emptyState = document.getElementById('sop-empty-state');
|
||||
|
||||
// Tampilkan pesan jika data SOP kosong atau gagal dimuat
|
||||
if (!sops || sops.length === 0) {
|
||||
container.innerHTML = `<p class="empty-state-msg">Data SOP tidak tersedia saat ini.</p>`;
|
||||
container.innerHTML = `<p class="empty-state-msg">${t('grp_c4a_empty_sop', 'Data SOP tidak tersedia saat ini.')}</p>`;
|
||||
updateResultInfo(0, 0);
|
||||
if (emptyState) emptyState.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = sops.map(buildSopCard).join('');
|
||||
updateResultInfo(sops.length, sops.length);
|
||||
if (emptyState) emptyState.style.display = 'none';
|
||||
}
|
||||
|
||||
/** Update teks info jumlah hasil */
|
||||
@@ -276,16 +360,18 @@ function updateResultInfo(shown, total) {
|
||||
const el = document.getElementById('sop-result-info');
|
||||
if (!el) return;
|
||||
if (shown === total) {
|
||||
el.textContent = `Menampilkan ${total} SOP`;
|
||||
const allTemplate = t('grp_c4a_sop_showing_all', 'Menampilkan {total} SOP');
|
||||
el.textContent = allTemplate.replace('{total}', total);
|
||||
} else {
|
||||
el.textContent = `Menampilkan ${shown} dari ${total} SOP`;
|
||||
const filteredTemplate = t('grp_c4a_sop_showing_filtered', 'Menampilkan {shown} dari {total} SOP');
|
||||
el.textContent = filteredTemplate.replace('{shown}', shown).replace('{total}', total);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Memfilter dan menyaring kartu SOP berdasarkan query pencarian dan chip filter aktif.
|
||||
*/
|
||||
function applySopFilter() {
|
||||
export function applySopFilter() {
|
||||
const container = document.getElementById('sop-cards-container');
|
||||
if (!container) return;
|
||||
|
||||
@@ -310,11 +396,17 @@ function applySopFilter() {
|
||||
});
|
||||
|
||||
const emptyState = document.getElementById('sop-empty-state');
|
||||
if (emptyState) emptyState.style.display = visibleCount === 0 ? 'flex' : 'none';
|
||||
if (emptyState) {
|
||||
// Tampilkan "SOP tidak ditemukan" HANYA jika memang ada data SOP tetapi hasil filter 0.
|
||||
// Jika data SOP memang kosong (tidak ada card sama sekali di data source), sembunyikan.
|
||||
const hasData = container._sopData && container._sopData.length > 0;
|
||||
emptyState.style.display = (hasData && 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
|
||||
@@ -552,10 +644,11 @@ export function renderFaqSection(faqs) {
|
||||
if (!container) return;
|
||||
|
||||
if (!faqs || faqs.length === 0) {
|
||||
container.innerHTML = `<p class="empty-state-msg">Data FAQ tidak tersedia saat ini.</p>`;
|
||||
container.innerHTML = `<p class="empty-state-msg">${t('grp_c4a_empty_faq', 'Data FAQ tidak tersedia saat ini.')}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const delayClasses = ['reveal-delay-1', 'reveal-delay-2', 'reveal-delay-3'];
|
||||
|
||||
const itemsHTML = faqs.map((faq, index) => {
|
||||
@@ -563,11 +656,11 @@ export function renderFaqSection(faqs) {
|
||||
return `
|
||||
<div class="faq-item reveal ${delayClass}">
|
||||
<div class="faq-question">
|
||||
<span class="faq-question-text">${faq.question}</span>
|
||||
<span class="faq-question-text">${loc(faq.question)}</span>
|
||||
<div class="faq-icon">+</div>
|
||||
</div>
|
||||
<div class="faq-answer">
|
||||
<div class="faq-answer-inner">${faq.answer}</div>
|
||||
<div class="faq-answer-inner">${loc(faq.answer)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
Reference in New Issue
Block a user