JustPaste
HomeCategoriesAboutDonateContactTerms of UsePrivacy Policy
JustPaste

Free online notepad — write and share instantly

Navigate

  • Home
  • Timeline
  • Categories

Info

  • About
  • Donate
  • Contact

Legal

  • Terms of Use
  • Privacy Policy

© 2026 JustPaste.app. All rights reserved.

Made with ♥ by JustPaste

pnp111 | JustPaste.app
20 days ago3 views
👨‍💻Programming

pnp111

public List<SoalAgendaDetailModel> GetSoalAgenda(int agendaId)
		{
			var list = new List<SoalAgendaDetailModel>();
			var byId = new Dictionary<int, SoalAgendaDetailModel>();

			using (var conn = new SqlConnection(_connStr))
			{
				conn.Open();

				string qSoal = @"
					SELECT ds.ID, a.NamaAgenda, bs.Pertanyaan, bs.KategoriSoal, bs.KunciJawabanLabel, ds.IsAnulir
					FROM ISEE_PNP_DistribusiSoal ds
					INNER JOIN ISEE_PNP_AgendaUjian a ON a.ID = ds.AgendaUjianID
					INNER JOIN ISEE_PNP_BankSoal bs ON ds.BankSoalID = bs.ID
					WHERE ds.AgendaUjianID = @id
					ORDER BY bs.KategoriSoal, bs.Pertanyaan";

				using (var cmd = new SqlCommand(qSoal, conn))
				{
					cmd.Parameters.AddWithValue("@id", agendaId);
					using (var reader = cmd.ExecuteReader())
					{
						while (reader.Read())
						{
							var item = new SoalAgendaDetailModel
							{
								DistribusiSoalID = reader.GetInt32(0),
								NamaAgenda = reader.IsDBNull(1) ? "" : reader.GetString(1),
								Pertanyaan = reader.IsDBNull(2) ? "" : reader.GetString(2),
								KategoriSoal = reader.IsDBNull(3) ? "" : reader.GetString(3),
								KunciJawaban = reader.IsDBNull(4) ? "" : reader.GetString(4),
								IsAnulir = !reader.IsDBNull(5) && reader.GetBoolean(5),
								OpsiList = new List<SoalAgendaOpsiModel>()
							};
							list.Add(item);
							byId[item.DistribusiSoalID] = item;
						}
					}
				}

				if (list.Count == 0) return list;

				string qOpsi = @"
					SELECT ds.ID, o.LabelOpsi, o.TeksOpsi
					FROM ISEE_PNP_DistribusiSoal ds
					INNER JOIN ISEE_PNP_OpsiSoal o ON o.BankSoalID = ds.BankSoalID
					WHERE ds.AgendaUjianID = @id
					ORDER BY ds.ID, LEN(o.LabelOpsi), o.LabelOpsi";

				using (var cmdOpsi = new SqlCommand(qOpsi, conn))
				{
					cmdOpsi.Parameters.AddWithValue("@id", agendaId);
					using (var r = cmdOpsi.ExecuteReader())
					{
						while (r.Read())
						{
							SoalAgendaDetailModel soal;
							if (!byId.TryGetValue(r.GetInt32(0), out soal)) continue;
							soal.OpsiList.Add(new SoalAgendaOpsiModel
							{
								LabelOpsi = r.IsDBNull(1) ? "" : r.GetString(1),
								TeksOpsi = r.IsDBNull(2) ? "" : r.GetString(2)
							});
						}
					}
				}
			}
			return list;
		}

        public void AnulirSoal(int distribusiSoalId, bool isAnulir, string userId)
        {
            using (var conn = new SqlConnection(_connStr))
            {
                conn.Open();
                using (var trans = conn.BeginTransaction())
                {
                    try
                    {
                        string q = @"
                            UPDATE ISEE_PNP_DistribusiSoal
                            SET IsAnulir = @isAnulir,
                                ApprovalStatus = 'Pending'
                            WHERE ID = @id";

                        using (var cmd = new SqlCommand(q, conn, trans))
                        {
                            cmd.Parameters.AddWithValue("@isAnulir", isAnulir);
                            cmd.Parameters.AddWithValue("@id", distribusiSoalId);
                            cmd.ExecuteNonQuery();
                        }

                        string namaSoal = "";
                        string namaAgenda = "";
                        using (var cmdName = new SqlCommand(@"
                            SELECT bs.Pertanyaan, a.NamaAgenda
                            FROM ISEE_PNP_DistribusiSoal ds
                            INNER JOIN ISEE_PNP_BankSoal bs ON ds.BankSoalID = bs.ID
                            INNER JOIN ISEE_PNP_AgendaUjian a ON ds.AgendaUjianID = a.ID
                            WHERE ds.ID = @id", conn, trans))
                        {
                            cmdName.Parameters.AddWithValue("@id", distribusiSoalId);
                            using (var reader = cmdName.ExecuteReader())
                            {
                                if (reader.Read())
                                {
                                    namaSoal = reader.IsDBNull(0) ? "" : reader.GetString(0);
                                    namaAgenda = reader.IsDBNull(1) ? "" : reader.GetString(1);
                                }
                            }
                        }

                        // Insert Approval Log
                        string actionText = isAnulir ? "Anulir" : "Batalkan Anulir";
                        string desc = actionText + " Soal: " + namaSoal;
                        string qLog = @"
                            INSERT INTO ISEE_PNP_ApprovalLog
                            (RequestType, ReferenceID, RequestDescription, ApprovalStatus, RequestedBy, RequestedAt)
                            VALUES (@type, @refId, @desc, 'Pending', @uid, GETDATE())";

                        using (var cmdLog = new SqlCommand(qLog, conn, trans))
                        {
                            cmdLog.Parameters.AddWithValue("@type", "AnulirSoal");
                            cmdLog.Parameters.AddWithValue("@refId", distribusiSoalId);
                            cmdLog.Parameters.AddWithValue("@desc", desc);
                            cmdLog.Parameters.AddWithValue("@uid", userId);
                            cmdLog.ExecuteNonQuery();
                        }

                        // Insert History Log
                        // Aturan penilaian: soal anulir tidak memberi nilai otomatis; jawaban benar tetap
                        // benar, jawaban salah tetap salah (nilai dihitung ulang saat approval disetujui).
                        string historyAction = isAnulir
                            ? string.Format("Menganulir Soal: '{0}' pada Agenda '{1}' (Jawaban peserta tetap dinilai: benar tetap benar, salah tetap salah)", namaSoal, namaAgenda)
                            : string.Format("Membatalkan Anulir Soal: '{0}' pada Agenda '{1}' (Soal dinilai normal kembali)", namaSoal, namaAgenda);

                        InsertHistoryLogInTransaction(conn, trans, userId, historyAction, "DistribusiSoal", distribusiSoalId.ToString());

                        trans.Commit();
                    }
                    catch
                    {
                        trans.Rollback();
                        throw;
                    }
                }
            }
        }

        public List<StaffModel> GetStaffList()
        {
            var list = new List<StaffModel>();
            using (var conn = new SqlConnection(_connStr))
            {
                var cmd = new SqlCommand(
                    "SELECT Domain_ID, Full_Name FROM v_ISEE_STAFF_DATA WITH (NOLOCK) WHERE Status = 'Aktif' ORDER BY Full_Name",
                    conn);

                try
                {
                    conn.Open();
                    using (var reader = cmd.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            string domainId = reader.IsDBNull(0) ? "" : reader.GetString(0);
                            string fullName = reader.IsDBNull(1) ? "Tanpa Nama" : reader.GetString(1);

                            if (!string.IsNullOrWhiteSpace(domainId))
                            {
                                list.Add(new StaffModel
                                {
                                    DomainID = domainId,
                                    FullName = fullName
                                });
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    throw new Exception("Database Error pada GetStaffList: " + ex.Message);
                }
            }
            return list;
        }

        public void StopAgenda(int agendaId, string userId)
        {
            using (var conn = new SqlConnection(_connStr))
            {
                conn.Open();
                using (var trans = conn.BeginTransaction())
                {
                    try
                    {
                        string q = @"
                            UPDATE ISEE_PNP_AgendaUjian
                            SET Status = 'Berhenti',
                                ApprovalStatus = 'Pending',
                                UpdatedBy = @uid,
                                UpdatedAt = GETDATE()
                            WHERE ID = @id";

                        using (var cmd = new SqlCommand(q, conn, trans))
                        {
                            cmd.Parameters.AddWithValue("@uid", userId);
                            cmd.Parameters.AddWithValue("@id", agendaId);
                            cmd.ExecuteNonQuery();
                        }

                        string namaAgenda = "";
                        using (var cmdName = new SqlCommand("SELECT NamaAgenda FROM ISEE_PNP_AgendaUjian WHERE ID = @id", conn, trans))
                        {
                            cmdName.Parameters.AddWithValue("@id", agendaId);
                            namaAgenda = Convert.ToString(cmdName.ExecuteScalar()) ?? "";
                        }

                        // Insert Approval Log
                        string qLog = @"
                            INSERT INTO ISEE_PNP_ApprovalLog
                            (RequestType, ReferenceID, RequestDescription, ApprovalStatus, RequestedBy, RequestedAt)
                            VALUES (@type, @refId, @desc, 'Pending', @uid, GETDATE())";

                        using (var cmdLog = new SqlCommand(qLog, conn, trans))
                        {
                            cmdLog.Parameters.AddWithValue("@type", "StopAgenda");
                            cmdLog.Parameters.AddWithValue("@refId", agendaId);
                            cmdLog.Parameters.AddWithValue("@desc", "Stop Agenda: " + namaAgenda);
                            cmdLog.Parameters.AddWithValue("@uid", userId);
                            cmdLog.ExecuteNonQuery();
                        }

                        // Insert History Log
                        string historyAction = string.Format("Menghentikan Agenda Distribusi '{0}'", namaAgenda);
                        InsertHistoryLogInTransaction(conn, trans, userId, historyAction, "DistribusiSoal", agendaId.ToString());

                        trans.Commit();
                    }
                    catch
                    {
                        trans.Rollback();
                        throw;
                    }
                }
            }
        }
    }
}



8. DistribusiSoalHandler.ashx
<%@ WebHandler Language="C#" Class="DistribusiSoalHandler" %>

using System;
using System.IO;
using System.Web;
using System.Web.Script.Serialization;
using System.Web.SessionState;
using Models.pnp.ujian;
using Repositories.pnp.ujian;

public class DistribusiSoalHandler : IHttpHandler, IRequiresSessionState
{
    private const string CH_DISTRIBUSI = "DistribusiSoal";
    private const string CH_LOG_DISTRIBUSI = "LogDistribusiSoal";

    public void ProcessRequest(HttpContext context)
    {
        context.Response.ContentType = "application/json";
        JavaScriptSerializer json = new JavaScriptSerializer { MaxJsonLength = Int32.MaxValue };

        try
        {
            string action = Convert.ToString(context.Request["action"]);
            DistribusiSoalRepository repo = new DistribusiSoalRepository();

            string userId = "SYSTEM";
            if (context.Session != null && context.Session["DomainID"] != null)
            {
                userId = context.Session["DomainID"].ToString();
            }

            object result = null;

            switch (action)
            {
                case "GetAgendaList":
                    result = repo.GetAgendaList(
                        context.Request["periode"],
                        context.Request["namaAgenda"],
                        context.Request["jenisSoalIds"],
                        context.Request["kategori"],
                        context.Request["status"]);
                    break;

                case "GetAgendaById":
                    result = repo.GetAgendaById(Convert.ToInt32(context.Request["id"]));
                    break;

                case "GetSoalAgenda":
                    result = repo.GetSoalAgenda(Convert.ToInt32(context.Request["id"]));
                    break;

                case "SaveAgenda":
                    AgendaUjianModel payload = ReadPayload<AgendaUjianModel>(context);
                    if (payload == null)
                    {
                        throw new Exception("Payload kosong atau format JSON salah.");
                    }
                    bool isReschedule = Convert.ToString(context.Request["isReschedule"]) == "true";
                    repo.SaveAgenda(payload, userId, isReschedule);
                    result = new { success = true };
                    PnpDataHub.Notify(CH_DISTRIBUSI);
                    PnpDataHub.Notify(CH_LOG_DISTRIBUSI);
                    PnpDataHub.Notify("ParameterSurvey");
                    NotificationService.RefreshNow();
                    break;

                case "AnulirSoal":
                    repo.AnulirSoal(
                        Convert.ToInt32(context.Request["id"]),
                        Convert.ToString(context.Request["isAnulir"]) == "true",
                        userId
                    );
                    result = new { success = true };
                    PnpDataHub.Notify(CH_DISTRIBUSI);
                    PnpDataHub.Notify(CH_LOG_DISTRIBUSI);
                    NotificationService.RefreshNow();
                    break;

                case "StopAgenda":
                    repo.StopAgenda(Convert.ToInt32(context.Request["id"]), userId);
                    result = new { success = true };
                    PnpDataHub.Notify(CH_DISTRIBUSI);
                    PnpDataHub.Notify(CH_LOG_DISTRIBUSI);
                    NotificationService.RefreshNow();
                    break;

                case "GetStaffList":
                    result = repo.GetStaffList();
                    break;

                case "GetLogDistribusiSoal":
                    DateTime? dateFrom = null, dateTo = null;
                    DateTime tempDate;
                    if (!string.IsNullOrEmpty(context.Request["dateFrom"]) && DateTime.TryParse(context.Request["dateFrom"], out tempDate))
                        dateFrom = tempDate;
                    if (!string.IsNullOrEmpty(context.Request["dateTo"]) && DateTime.TryParse(context.Request["dateTo"], out tempDate))
                        dateTo = tempDate;
                    result = repo.GetLogDistribusiSoal(dateFrom, dateTo);
                    break;

                default:
                    result = new { success = false, error = "Action tidak dikenali: " + action };
                    break;
            }

            context.Response.Write(json.Serialize(result));
        }
        catch (Exception ex)
        {
            context.Response.StatusCode = 500;
            context.Response.Write(json.Serialize(new { success = false, error = ex.Message }));
        }
    }

    private T ReadPayload<T>(HttpContext context)
    {
        context.Request.InputStream.Position = 0;
        using (StreamReader reader = new StreamReader(context.Request.InputStream))
        {
            string body = reader.ReadToEnd();
            if (string.IsNullOrEmpty(body)) return default(T);
            JavaScriptSerializer serializer = new JavaScriptSerializer { MaxJsonLength = Int32.MaxValue };
            return serializer.Deserialize<T>(body);
        }
    }

    public bool IsReusable
    {
        get { return false; }
    }
}




9. pnp.aspx
<%@ Page Language="C#" MasterPageFile="~/iSee.master" EnableEventValidation="false" AutoEventWireup="true" CodeFile="pnp.aspx.cs" Inherits="PNP" Title="iSee" MaintainScrollPositionOnPostback="true" %>
<%@ MasterType VirtualPath="~/iSee.master" %>
<asp:Content ContentPlaceHolderID="Content" runat="server">

<!-- Hidden Data User -->
<asp:Label ID="lblDomainID" Visible="false" runat="server" />
<asp:Label ID="lblProfile" Visible="false" runat="server" />
<asp:Label ID="lblStaffID" Visible="false" runat="server" />
<asp:Label ID="lblFullName" Visible="false" runat="server" />
<asp:Label ID="lblUserRole" Visible="false" runat="server" />
<script>
	window.currentTrainerName = "<%= System.Web.HttpUtility.JavaScriptStringEncode(lblFullName.Text) %>";
	window.PNP_USER_ROLE = "<%= System.Web.HttpUtility.JavaScriptStringEncode(lblUserRole.Text) %>";
</script>

<!-- CSS -->
<link href="<%= ResolveUrl("~/css/select2.min.css") %>" rel="stylesheet" />
<link href="<%= ResolveUrl("~/css/all.min.css") %>" rel="stylesheet" />
<link href="<%= ResolveUrl("~/css/pnp.css") %>" rel="stylesheet" />

<link href="<%= ResolveUrl("~/css/monthpickr/monthpickr.min.css") %>" rel="stylesheet" />
<link href="<%= ResolveUrl("~/css/monthpickr/monthpickr.css") %>" rel="stylesheet" />
<script src="<%= ResolveUrl("~/scripts/monthpickr/monthpickr.min.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/monthpickr/monthpickr.js") %>"></script>

<style>
	.footer {
		display: none;
	}
	.layout { margin-top: -20px; }
</style>

<div class="layout">

<!-- SIDEBAR -->
<aside class="sidebar" id="pnpSidebar">
	<div class="logo">
		<h2>PNP</h2>
		<span>Exam System</span>
	</div>
	
	<ul class="menu">

		<!-- 1. MENU ADMIN MAKER (Trainer) -->
		<div ID="MenuAdminMaker" runat="server" visible="false">
			<li class="menu-item" data-page="dashboard"><a href="javascript:void(0)">Dashboard</a></li>
			<li class="menu-item dropdown">
				<div class="dropdown-toggle"><span>Administrasi Pengaturan</span><span class="arrow">&#9662;</span></div>
				<ul class="submenu">
					<li data-page="administrasi-pengaturan/parameter-kelompok-distribusi"><a href="javascript:void(0)">Parameter Kelompok Distribusi</a></li>
					<li data-page="administrasi-pengaturan/parameter-scoring"><a href="javascript:void(0)">Parameter Scoring</a></li>
					<li data-page="administrasi-pengaturan/parameter-survey"><a href="javascript:void(0)">Parameter Survey</a></li>
					<li data-page="notifikasi/pengaturan-notifikasi"><a href="javascript:void(0)">Pengaturan Notifikasi</a></li>
					<li data-page="running-text/pengaturan-running-text"><a href="javascript:void(0)">Pengaturan Running Text</a></li>
				</ul>
			</li>
			<li class="menu-item" data-page="bank-soal/bank-soal"><a href="javascript:void(0)">Bank Soal</a></li>
			<li class="menu-item" data-page="distribusi-soal/distribusi-soal"><a href="javascript:void(0)">Distribusi Soal</a></li>
			<li class="menu-item" data-page="kode-referensi/kode-referensi"><a href="javascript:void(0)">Kode Referensi</a></li>
			<li class="menu-item dropdown">
				<div class="dropdown-toggle"><span>Report</span><span class="arrow">&#9662;</span></div>
				<ul class="submenu">
					<li data-page="report/report-peserta"><a href="javascript:void(0)">Report Peserta</a></li>
					<li data-page="report/report-agenda-distribusi"><a href="javascript:void(0)">Report Agenda Distribusi</a></li>
					<li data-page="report/report-soal-distribusi"><a href="javascript:void(0)">Report Soal Distribusi</a></li>
					<li data-page="report/report-jenis-kategori-soal"><a href="javascript:void(0)">Report Jenis dan Kategori Soal</a></li>
					<li data-page="report/report-soal-distribusi-agenda"><a href="javascript:void(0)">Report Soal Distribusi Agenda</a></li>
					<li data-page="report/report-survey"><a href="javascript:void(0)">Report Survey</a></li>
				</ul>
			</li>
		</div>

		<!-- 2. MENU ADMIN APPROVAL (CN014136) -->
		<div ID="MenuAdminApproval" runat="server" visible="false">
			<li class="menu-item" data-page="dashboard"><a href="javascript:void(0)">Dashboard</a></li>
			<li class="menu-item" data-page="approval/approval"><a href="javascript:void(0)">Need Approval List</a></li>
			<li class="menu-item dropdown">
				<div class="dropdown-toggle"><span>Report</span><span class="arrow">&#9662;</span></div>
				<ul class="submenu">
					<li data-page="report/report-peserta"><a href="javascript:void(0)">Report Peserta</a></li>
					<li data-page="report/report-agenda-distribusi"><a href="javascript:void(0)">Report Agenda Distribusi</a></li>
					<li data-page="report/report-soal-distribusi"><a href="javascript:void(0)">Report Soal Distribusi</a></li>
					<li data-page="report/report-jenis-kategori-soal"><a href="javascript:void(0)">Report Jenis dan Kategori Soal</a></li>
					<li data-page="report/report-soal-distribusi-agenda"><a href="javascript:void(0)">Report Soal Distribusi Agenda</a></li>
					<li data-page="report/report-survey"><a href="javascript:void(0)">Report Survey</a></li>
				</ul>
			</li>
		</div>

		<!-- 3. MENU PESERTA (Agent, Admin, Supervisor) -->
		<div ID="MenuPeserta" runat="server" visible="false">
			<li class="menu-item" data-page="peserta/dashboard-peserta"><a href="javascript:void(0)">Dashboard</a></li>
			<li class="menu-item" data-page="peserta/ujian-peserta"><a href="javascript:void(0)">Tes PNP</a></li>
			<li class="menu-item" data-page="peserta/pencapaian-pnp"><a href="javascript:void(0)">Pencapaian PNP</a></li>
		</div>

	</ul>
</aside>

<!-- Overlay shown only on mobile when sidebar is open -->
<div class="sidebar-overlay" id="sidebarOverlay"></div>

<!-- CONTENT AREA -->
<main class="pnp-content">
<div class="topbar">
<button type="button" id="btnToggleSidebar" class="sidebar-toggle-btn" aria-label="Buka menu" aria-expanded="false" aria-controls="pnpSidebar">
<span class="hamburger-icon"></span>
</button>
<h2 id="page-title">Dashboard</h2>
</div>

<div id="main-content">
<div class="loading-spinner">Memuat halaman...</div>
</div>
</main>

</div>

<!-- Hidden controls untuk generate template Excel -->
<asp:HiddenField ID="hfTemplateConfig" runat="server" ClientIDMode="Static" />
<asp:Button ID="btnGenerateTemplate" runat="server" ClientIDMode="Static" Style="display:none;" OnClick="btnGenerateTemplate_Click" />

<!-- 1. CORE LIBRARIES (Wajib dimuat pertama kali) -->
<script src="<%= ResolveUrl("~/scripts/jquery-3.7.1.min.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/select2.min.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/jquery.signalR-2.4.3.min.js") %>"></script>
<script src="<%= ResolveUrl("~/signalr/hubs") %>"></script>
<script src="<%= ResolveUrl("~/scripts/xlsx.full.min.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/core/pnp-cache.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/core/pnp-table.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/core/pnp-validate.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/core/pnp-realtime.js") %>"></script>

<!-- 2. MODULE SCRIPTS -->
<script src="<%= ResolveUrl("~/scripts/pnp/dashboard/dashboard.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/dashboard/charts.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/administrasi-pengaturan/parameter-kelompok-distribusi.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/administrasi-pengaturan/parameter-scoring.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/administrasi-pengaturan/parameter-survey.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/notifikasi/pengaturan-notifikasi.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/running-text/pengaturan-running-text.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/bank-soal/upload-soal.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/bank-soal/bank-soal.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/distribusi-soal/distribusi-soal.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/approval/approval.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/kodereferensi/kode-referensi.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/peserta/ujian-peserta.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/peserta/pencapaian-pnp.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/monitoring/monitoring.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/report/report-survey.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/report/report-peserta.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/report/report-agenda-distribusi.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/report/report-soal-distribusi.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/report/report-jenis-kategori-soal.js") %>"></script>
<script src="<%= ResolveUrl("~/scripts/pnp/report/report-soal-distribusi-agenda.js") %>"></script>

<!-- 3. MAIN ROUTER -->
<script src="<%= ResolveUrl("~/scripts/pnp.js") %>"></script>

<script>
    window.PNP_DOMAIN_ID = '<%= System.Web.HttpUtility.JavaScriptStringEncode(lblDomainID.Text) %>';
    window.PNP_STAFF_ID = '<%= System.Web.HttpUtility.JavaScriptStringEncode(lblStaffID.Text) %>';

    console.log('=== PNP USER INFO ===');
    console.log('DomainID:', window.PNP_DOMAIN_ID);
    console.log('StaffID :', window.PNP_STAFF_ID);
    console.log('FullName:', window.currentTrainerName);
    console.log('Role    :', window.PNP_USER_ROLE);
</script>

</asp:Content>



10. pnp.js
// ============================================
// GLOBAL SVG ICON SYSTEM
// ============================================
window.PNP_ICONS = {
	search: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>',
	edit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>',
	save: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><path d="M17 21v-8H7v8"/><path d="M7 3v5h8"/></svg>',
	close: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
	reset: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>',
	trash: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>',
	plus: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>',
	folder: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4.5l2 2H19a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/></svg>',
	file: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>',
	check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>',
	inbox: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>',
	home: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9.5 12 3l9 6.5V21a1 1 0 0 1-1 1h-5v-7h-6v7H4a1 1 0 0 1-1-1z"/></svg>',
	calendar: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
	clipboard: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1" ry="1"/></svg>',
	user: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
	users: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>',
	cog: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
	minus: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/></svg>',
	warning: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
	info: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
	lock: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>',
	list: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>',
	download: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
	filter: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>',
	dice: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
		<path d="M8 4L17 6.5L11.5 10L3 7.5Z"/>
		<path d="M3 7.5V16.5L11.5 20V10Z"/>
		<path d="M11.5 10V20L17 16.5V6.5Z"/>
		<circle cx="7.2" cy="11" r="0.8" fill="currentColor" stroke="none"/>
		<circle cx="9.5" cy="14" r="0.8" fill="currentColor" stroke="none"/>
		<circle cx="13.8" cy="11.5" r="0.8" fill="currentColor" stroke="none"/>
		<circle cx="14.2" cy="15" r="0.8" fill="currentColor" stroke="none"/>
	</svg>`,
	clock: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
	pause: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>',
	eyeoff: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>',
	stop: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/></svg>',
	history: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></svg>',
	usersadd: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg>',
	chevrondown: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>',
	ban: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>',
	upload: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="M7 9l5-5 5 5"/><path d="M20 16v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-3"/></svg>',
	alert: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
	thumbsup: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg>',
	thumbsdown: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"/></svg>',
	eye: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>',
	pie: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.21 15.89A10 10 0 1 1 8 2.83"/><path d="M22 12A10 10 0 0 0 12 2v10z"/></svg>'
};

window.pnpIcoHtml = function (name, size) {
	size = size || 16;
	var raw = (window.PNP_ICONS && window.PNP_ICONS[name]) || '';
	return raw.replace('<svg ', '<svg width="' + size + '" height="' + size + '" ');
};

window.pnpIcon = function (name, size) {
	return '<span class="pnp-ico">' + window.pnpIcoHtml(name, size) + '</span>';
};

window.paintPnpIcons = function (root) {
	var scope = root || document;
	if (!scope || !scope.querySelectorAll) return;
	var nodes = scope.querySelectorAll('[data-ic]');
	for (var i = 0; i < nodes.length; i++) {
		var n = nodes[i];
		var name = n.getAttribute('data-ic');
		var size = parseInt(n.getAttribute('data-size') || '16', 10);
		n.innerHTML = window.pnpIcoHtml(name, size);
	}
};

// ============================================
// DIRTY GUARD (konfirmasi tinggalkan halaman)
// ============================================
window.PNP_DIRTY = {
	_check: null,
	register: function (fn, opts) { this._check = fn; this._opts = opts || {}; },
	clear: function () {
		if (typeof this._cleanup === "function") this._cleanup();
		this._check = null;
		this._opts = null;
		this._cleanup = null;
	},
	options: function () { return this._opts || {}; },
	isDirty: function () {
		try { return typeof this._check === "function" && !!this._check(); }
		catch (e) { console.error(e); return false; }
	}
};

// ============================================
// PNP ROUTER
// ============================================
const PNP = {

	_loadToken: 0,
	
	_currentPage: null,

	confirmLeave: function (proceed, onCancel) {
		if (!window.PNP_DIRTY.isDirty()) {
			proceed();
			return;
		}
		var opts = window.PNP_DIRTY.options();
		showConfirmModal(function () {
			window.PNP_DIRTY.clear();
			proceed();
		}, {
			title: opts.title || "Tinggalkan Halaman?",
			message: opts.message || "Perubahan Anda belum disimpan. Yakin ingin meninggalkan menu ini tanpa menyimpan?",
			confirmText: opts.confirmText || "Ya, Tinggalkan",
			cancelText: "Batal",
			variant: "warning",
			onCancel: onCancel
		});
	},

	roleLabels: {
		AdminMaker: "Admin Maker",
		AdminApproval: "Admin Approval",
		Peserta: "Peserta"
	},

	pageAccess: (function () {
		const MAKER = "AdminMaker";
		const APPROVAL = "AdminApproval";
		const PESERTA = "Peserta";
		const REPORT = [MAKER, APPROVAL];

		return {
			"dashboard": [MAKER, APPROVAL ],
			"peserta/dashboard-peserta": [PESERTA],

			// Admin Maker
			"administrasi-pengaturan/parameter-kelompok-distribusi": [MAKER],
			"administrasi-pengaturan/parameter-scoring": [MAKER],
			"administrasi-pengaturan/parameter-survey": [MAKER],
			"notifikasi/pengaturan-notifikasi": [MAKER],
			"running-text/pengaturan-running-text": [MAKER],
			"bank-soal/bank-soal": [MAKER],
			"bank-soal/upload-soal": [MAKER],
			"bank-soal/detail-bank-soal/detail-kategori-soal": [MAKER],
			"distribusi-soal/distribusi-soal": [MAKER],
			"distribusi-soal/detail-distribusi-soal/create-agenda": [MAKER],
			"distribusi-soal/detail-distribusi-soal/reschedule-agenda": [MAKER],
			"distribusi-soal/detail-distribusi-soal/soal-agenda": [MAKER],
			"kode-referensi/kode-referensi": [MAKER],

			// Report (Admin Maker dan Admin Approval)
			"report/report-peserta": REPORT,
			"report/report-agenda-distribusi": REPORT,
			"report/report-soal-distribusi": REPORT,
			"report/report-jenis-kategori-soal": REPORT,
			"report/report-soal-distribusi-agenda": REPORT,
			"report/report-survey": REPORT,

			// Admin Approval
			"approval/approval": [APPROVAL],

			// Peserta
			"peserta/ujian-peserta": [PESERTA],
			"peserta/pencapaian-pnp": [PESERTA]
		};
	})(),

	pageModules: {

		"dashboard": () => { if (typeof initConsole === "function") initConsole(); },
		
		"peserta/dashboard-peserta": () => { if (typeof initConsole === "function") initConsole(); },

		"administrasi-pengaturan/parameter-kelompok-distribusi": () => {

			if (typeof initParameterKelompokDistribusi === "function") {

				initParameterKelompokDistribusi();

			}

		},

		"administrasi-pengaturan/parameter-scoring": () => {

			if (typeof initParameterScoring === "function") initParameterScoring();

		},

		"administrasi-pengaturan/parameter-survey": () => {

			if (typeof SurveyApp.init === "function") SurveyApp.init();

		},

		"notifikasi/pengaturan-notifikasi": () => {

			if (typeof initPengaturanNotifikasi === "function") initPengaturanNotifikasi();

		},
		
		"running-text/pengaturan-running-text": () => {

			if (typeof initPengaturanRunningText === "function") initPengaturanRunningText();

		},
		

		"bank-soal/bank-soal": () => { if (typeof initBankSoal === "function") initBankSoal(); },

		"bank-soal/upload-soal": () => { if (typeof initUploadSoal === "function") initUploadSoal(); },

		"bank-soal/detail-bank-soal/detail-kategori-soal": () => {

			if (typeof initBankSoalDetail === "function") initBankSoalDetail();

		},

		"distribusi-soal/distribusi-soal": () => { if (typeof initDistribusiSoal === "function") initDistribusiSoal(); },

		"distribusi-soal/detail-distribusi-soal/create-agenda": () => { if (typeof CreateAgendaApp !== "undefined") CreateAgendaApp.init(); },

		"distribusi-soal/detail-distribusi-soal/reschedule-agenda": () => { if (typeof initRescheduleAgenda === "function") initRescheduleAgenda(); },

		"distribusi-soal/detail-distribusi-soal/soal-agenda": () => { if (typeof initSoalAgenda === "function") initSoalAgenda(); },

		"kode-referensi/kode-referensi": () => { if (typeof initKodeReferensi === "function") initKodeReferensi(); },

		"report/report-peserta": () => { if (typeof initReportPeserta === "function") initReportPeserta(); },

		"report/report-agenda-distribusi": () => { if (typeof initReportAgendaDistribusi === "function") initReportAgendaDistribusi(); },

		"report/report-soal-distribusi": () => { if (typeof initReportSoalDistribusi === "function") initReportSoalDistribusi(); },

		"report/report-jenis-kategori-soal": () => { if (typeof initReportJenisKategoriSoal === "function") initReportJenisKategoriSoal(); },

		"report/report-soal-distribusi-agenda": () => { if (typeof initReportSoalDistribusiAgenda === "function") initReportSoalDistribusiAgenda(); },

		"report/report-survey": () => { if (typeof ReportSurvey.init === "function") ReportSurvey.init(); },

		"approval/approval": () => { if (typeof initApproval === "function") initApproval(); },

		"peserta/ujian-peserta": () => { if (typeof initUjianPeserta === "function") initUjianPeserta(); },

		"peserta/pencapaian-pnp": () => { if (typeof initPencapaianPnp === "function") initPencapaianPnp(); }

	},

	// ------------------------------------------------------------------
	// KONTEN HALAMAN STATE
	// ------------------------------------------------------------------
	statePages: {
		forbidden: {
			tone: "forbidden",
			code: "403 Forbidden",
			pageTitle: "Akses Ditolak",
			title: "Anda tidak memiliki akses ke halaman ini",
			desc: "Halaman ini tidak tersedia untuk peran Anda. Kembali ke dashboard, atau hubungi administrator jika Anda memerlukan akses.",
			help: "Merasa seharusnya punya akses? Hubungi administrator PNP dan sebutkan halaman yang Anda buka.",
			icon: '<svg viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
				'<path d="M32 6 10 14v16c0 13.5 9.2 24.6 22 28 12.8-3.4 22-14.5 22-28V14L32 6Z"/>' +
				'<rect x="22" y="28" width="20" height="15" rx="3"/>' +
				'<path d="M26 28v-4a6 6 0 0 1 12 0v4"/>' +
				'<circle cx="32" cy="35.5" r="1.6" fill="currentColor" stroke="none"/>' +
				'</svg>'
		},
		notfound: {
			tone: "notfound",
			code: "404 Not Found",
			pageTitle: "Halaman Tidak Ditemukan",
			title: "Halaman tidak ditemukan",
			desc: "Alamat yang Anda buka tidak tersedia di PNP. Periksa kembali alamatnya, atau buka halaman dari menu PNP.",
			help: "",
			icon: '<svg viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
				'<circle cx="28" cy="28" r="16"/>' +
				'<path d="m40 40 14 14"/>' +
				'<path d="M22.5 24a5.5 5.5 0 1 1 8.2 4.8c-1.7 1-2.7 2-2.7 3.7"/>' +
				'<circle cx="28" cy="37.5" r="1.5" fill="currentColor" stroke="none"/>' +
				'</svg>'
		},
		error: {
			tone: "error",
			code: "Gagal memuat",
			pageTitle: "Gagal Memuat Halaman",
			title: "Halaman gagal dimuat",
			desc: "Terjadi masalah saat memuat halaman ini. Coba muat ulang. Jika masalah berlanjut, hubungi administrator.",
			help: "",
			icon: window.PNP_ICONS.warning
		}
	},

	init: async function () {
		localStorage.removeItem("tempQuestions");
		this.initMenu();
		this.initSidebarToggle();

		if (typeof initBankSoalLockConnection === "function") {
			initBankSoalLockConnection();
		}

		let currentPage = this.getPageFromUrl();

		if (!currentPage) {
			const saved = localStorage.getItem("activePage");

			currentPage =
				(saved && this.resolveRoute(saved).status === "ok")
					? saved
					: this.getDefaultPageByRole();
		}
		this.loadPage(currentPage, true);
		this.restoreActiveMenu(currentPage);
	},

	initMenu: function () {
		const go = (page) => {
			this.confirmLeave(() => {
				this.loadPage(page);
				this.restoreActiveMenu(page);
				this.closeSidebar();
			});
		};

		document.querySelectorAll(".menu-item[data-page]").forEach(item => {
			item.addEventListener("click", () => go(item.dataset.page));
		});

		document.querySelectorAll(".submenu li").forEach(item => {
			item.addEventListener("click", (e) => {
				e.stopPropagation();
				go(item.dataset.page);
			});
		});

		document.querySelectorAll(".dropdown-toggle").forEach(toggle => {
			toggle.addEventListener("click", function (e) {
				e.stopPropagation();
				const currentDropdown = this.parentElement;
				document.querySelectorAll(".dropdown").forEach(drop => {
					if (drop !== currentDropdown) drop.classList.remove("open");
				});
				currentDropdown.classList.toggle("open");
			});
		});
	},

	initSidebarToggle: function () {
		const btn = document.getElementById("btnToggleSidebar");
		const sidebar = document.querySelector(".sidebar");
		const overlay = document.getElementById("sidebarOverlay");

		if (!btn || !sidebar || !overlay) return;

		btn.addEventListener("click", () => {
			const isOpen = sidebar.classList.toggle("open");
			overlay.classList.toggle("show", isOpen);
			btn.setAttribute("aria-expanded", isOpen ? "true" : "false");
		});
		overlay.addEventListener("click", () => this.closeSidebar());

		window.addEventListener("resize", () => {
			if (window.innerWidth > 768) this.closeSidebar();
		});
	},

	closeSidebar: function () {
		const sidebar = document.querySelector(".sidebar");
		const overlay = document.getElementById("sidebarOverlay");
		const btn = document.getElementById("btnToggleSidebar");
		if (sidebar) sidebar.classList.remove("open");
		if (overlay) overlay.classList.remove("show");
		if (btn) btn.setAttribute("aria-expanded", "false");
	},

	clearActive: function () {
		document.querySelectorAll(".menu-item, .submenu li").forEach(item => {
			item.classList.remove("active");
		});
		document.querySelectorAll(".dropdown").forEach(drop => {
			drop.classList.remove("open");
		});
	},

	normalizeRoute: function (route) {
		return String(route || "")
			.split("?")[0]
			.replace(/^\/+|\/+$/g, "")
			.toLowerCase();
	},

	getRouteMatchScore: function (currentRoute, menuRoute) {
		const currentSegments = this.normalizeRoute(currentRoute).split("/");
		const menuSegments = this.normalizeRoute(menuRoute).split("/");
		let score = 0;
		for (let i = 0; i < Math.min(currentSegments.length, menuSegments.length); i++) {
			if (currentSegments[i] !== menuSegments[i]) break;
			score++;
		}

		return score;
	},

	// ------------------------------------------------------------------
	// AKSES
	// ------------------------------------------------------------------
	getUserRole: function () {
		return String(window.PNP_USER_ROLE || "").trim();
	},
	
	getDefaultPageByRole: function () {
		const role = this.getUserRole();

		switch (role) {
			case "Peserta":
				return "peserta/dashboard-peserta";

			case "AdminApproval":
				return "dashboard";

			case "AdminMaker":
				return "dashboard";

			default:
				return "dashboard";
		}
	},

	resolveRoute: function (page) {
		const route = this.normalizeRoute(page);
		const registered = route !== "" &&
			Object.prototype.hasOwnProperty.call(this.pageAccess, route);

		if (!registered) return { status: "notfound", route: route };

		if (this.pageAccess[route].indexOf(this.getUserRole()) === -1) {
			return { status: "forbidden", route: route };
		}

		return { status: "ok", route: route };
	},

	escapeHtml: function (value) {
		return String(value == null ? "" : value)
			.replace(/&/g, "&amp;")
			.replace(/</g, "&lt;")
			.replace(/>/g, "&gt;")
			.replace(/"/g, "&quot;")
			.replace(/'/g, "&#39;");
	},

	renderStatePage: function (type, route, options) {
		options = options || {};
		const content = document.getElementById("main-content");
		if (!content) return;
		const cfg = this.statePages[type] || this.statePages.error;
		const fullPath = route ? "/pnp/" + route : "/pnp";
		const shownPath = fullPath.length > 120 ? fullPath.substring(0, 117) + "..." : fullPath;

		const rows = [
			'<li><span class="k">Halaman diminta</span><code class="v">' + this.escapeHtml(shownPath) + '</code></li>'
		];

		if (type === "forbidden") {
			const role = this.getUserRole();
			rows.push('<li><span class="k">Peran Anda</span><span class="v">' +
				this.escapeHtml(this.roleLabels[role] || role || "-") + '</span></li>');
		}

		rows.push('<li><span class="k">Kode</span><span class="v">' + this.escapeHtml(cfg.code) + '</span></li>');

		const backIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 12H5"/><path d="m12 19-7-7 7-7"/></svg>';

		let actions = "";

		if (type === "error" && typeof options.onRetry === "function") {
			actions += '<button type="button" class="btn-primary" data-state-action="retry">' + window.pnpIcon("reset", 16) + ' Muat ulang</button>';
			actions += '<button type="button" class="btn-secondary" data-state-action="home">' + window.pnpIcon("home", 16) + ' Ke dashboard</button>';
		} else {
			actions += '<button type="button" class="btn-primary" data-state-action="home">' + window.pnpIcon("home", 16) + ' Kembali ke dashboard</button>';

			if (window.history.length > 1) {
				actions += '<button type="button" class="btn-secondary" data-state-action="back">' + backIcon + ' Halaman sebelumnya</button>';
			}
		}

		content.innerHTML =
			'<div class="pnp-state pnp-state--' + cfg.tone + '">' +
				'<div class="pnp-state-card"' + (type === "notfound" ? "" : ' role="alert"') + '>' +
					'<div class="pnp-state-icon">' + cfg.icon + '</div>' +
					'<h3 class="pnp-state-title">' + cfg.title + '</h3>' +
					'<p class="pnp-state-desc">' + cfg.desc + '</p>' +
					'<ul class="pnp-state-meta">' + rows.join("") + '</ul>' +
					'<div class="pnp-state-actions">' + actions + '</div>' +
					(cfg.help ? '<p class="pnp-state-help">' + cfg.help + '</p>' : "") +
				'</div>' +
			'</div>';

		const bind = (name, handler) => {
			const el = content.querySelector('[data-state-action="' + name + '"]');
			if (el && typeof handler === "function") el.addEventListener("click", handler);
		};
		bind("home", () => window.goToPage(this.getDefaultPageByRole()));
		bind("back", () => window.history.back());
		bind("retry", options.onRetry);
		const title = document.getElementById("page-title");
		if (title) title.innerText = cfg.pageTitle;
	},

	restoreActiveMenu: function (page) {

		this.clearActive();
		if (this.resolveRoute(page).status !== "ok") return;
		const currentRoute = this.normalizeRoute(page);
		const menuItems = [...document.querySelectorAll(".menu [data-page]")];

		let activeItem = menuItems.find(item =>
			this.normalizeRoute(item.dataset.page) === currentRoute
		);

		if (!activeItem) {
			activeItem = menuItems
				.map(item => ({
					item,
					score: this.getRouteMatchScore(currentRoute, item.dataset.page)
				}))
				.filter(result => result.score > 0)
				.sort((a, b) => b.score - a.score)[0]?.item;
		}

		if (!activeItem) {
			console.warn(`Menu untuk route "${currentRoute}" tidak ditemukan.`);
			return;
		}

		activeItem.classList.add("active");
		const dropdown = activeItem.closest(".dropdown");

		if (dropdown) {
			dropdown.classList.add("active", "open");
		}
	},

	loadPage: function (page, skipPushState = false) {
		if (typeof window.__onLeavePage === "function") {
			try {
				window.__onLeavePage();
			} catch (e) {
				console.error(e);
			}
			window.__onLeavePage = null;
		}
		window.PNP_DIRTY.clear(); 

		const content = document.getElementById("main-content");
		const token = ++this._loadToken;
		const result = this.resolveRoute(page);
		const pagePath = result.route;
		this._currentPage = page;

		if (!skipPushState) {
			const cleanUrl = page === 'dashboard' ? '/pnp' : `/pnp/${page}`;
			history.pushState({ page: page }, "", cleanUrl);
		}

		if (result.status !== "ok") {
			this.clearActive();
			this.renderStatePage(result.status, pagePath);
			return;
		}

		content.innerHTML = '<div class="loading-spinner">Memuat halaman...</div>';
		localStorage.setItem("activePage", page);
		const filePath = "/content-pnp/" + pagePath + ".html";

		fetch(filePath)
			.then(response => {
				if (!response.ok) {
					const err = new Error("HTTP " + response.status + ": " + filePath);
					err.status = response.status;
					throw err;
				}
				return response.text();
			})

			.then(html => {
				if (token !== this._loadToken) return;
				content.innerHTML = html;
				this.initPageModule(pagePath);
				this.initSelect2(content);

				if (typeof window.paintPnpIcons === "function") {
					window.paintPnpIcons(content);
				}
				document.getElementById("page-title").innerText = this.formatTitle(pagePath.split("/").pop());
				if (typeof initAutoResize === "function") initAutoResize(content);
			})
			.catch(error => {
				if (token !== this._loadToken) return;
				console.error("Gagal load page:", page, error);

				if (error && error.status === 404) {
					this.renderStatePage("notfound", pagePath);
				} else {
					this.renderStatePage("error", pagePath, { onRetry: () => this.loadPage(page, true) });
				}
			});
	},

	getPageFromUrl: function () {
		const path = window.location.pathname;

		if (path.startsWith("/pnp/")) {
			return path.substring(5);
		}

		if (path === "/pnp" || path === "/pnp/") {
			return this.getDefaultPageByRole();
		}

		return null;
	},

	initPageModule: function (pagePath) {
		const moduleFn = this.pageModules[pagePath];

		if (typeof moduleFn === "function") {
			moduleFn();
		} else {
			console.warn(`Modul untuk halaman "${pagePath}" belum diimplementasikan.`);
		}
	},

	initSelect2: function (scope = document) {
		$(scope).find(".select2").each(function () {
			if ($(this).hasClass("select2-hidden-accessible")) {
				$(this).select2("destroy");
			}

			$(this).select2({
				width: "100%",
				placeholder: "Pilih",
				allowClear: true
			});
		});
	},

	formatTitle: function (text) {
		return text.replace(/-/g, " ").replace(/\b\w/g, l => l.toUpperCase());
	}
};

window.goToPage = function (page) {
	PNP.loadPage(page);
	PNP.restoreActiveMenu(page);
};

window.addEventListener("popstate", function () {
	const page = PNP.getPageFromUrl() || PNP.getDefaultPageByRole();
	const previous = PNP._currentPage;

	PNP.confirmLeave(function () {
		PNP.loadPage(page, true);
		PNP.restoreActiveMenu(page);
	}, function () {
		// batal: kembalikan URL ke halaman semula
		if (previous) {
			history.pushState({ page: previous }, "", previous === "dashboard" ? "/pnp" : "/pnp/" + previous);
		}
	});
});

// Peringatan bawaan browser saat refresh/tutup tab
window.addEventListener("beforeunload", function (e) {
	if (window.PNP_DIRTY.isDirty() && window.PNP_DIRTY.options().warnOnUnload !== false) {
		e.preventDefault();
		e.returnValue = "";
	}
});

document.addEventListener("DOMContentLoaded", function () {
	PNP.init();
});

document.addEventListener("keydown", function (event) {
	if (event.key !== "Enter") return;

	const target = event.target;
	const tag = target.tagName ? target.tagName.toUpperCase() : "";

	if (tag === "TEXTAREA") return;

	const isTextLikeInput = tag === "INPUT" && [
		"text", "search", "number", "email", "tel", "url", "password"
	].indexOf((target.type || "text").toLowerCase()) !== -1;
	const isSelect = tag === "SELECT";

	if (!isTextLikeInput && !isSelect) return;

	if (!target.closest("#main-content")) return;

	event.preventDefault();
	event.stopPropagation();

	const focusable = Array.prototype.slice.call(
		document.querySelectorAll(
			"#main-content input:not([disabled]):not([readonly]), " +
			"#main-content select:not([disabled])"
		)
	).filter(function (el) { return el.offsetParent !== null; });

	const idx = focusable.indexOf(target);
	if (idx !== -1 && idx < focusable.length - 1) {
		focusable[idx + 1].focus();
	} else {
		target.blur();
	}
}, true);

// ============================================
// TOAST MESSAGE
// ============================================
function showToast(message, success) {
	let toast = document.getElementById("toastTemplate");
	if (!toast) {
		toast = document.createElement("div");
		toast.id = "toastTemplate";
		document.body.appendChild(toast);
	}
	const icon = typeof window.pnpIcon === "function"
		? window.pnpIcon(success ? "check" : "close", 18)
		: "";
	toast.innerHTML = icon + "<span>" + message + "</span>";
	toast.className = "toast-template show " + (success ? "toast-success" : "toast-error");
	setTimeout(() => {
		toast.className = "toast-template";
	}, 4000);
}

window.getPageParams = function () {
	const directParams = new URLSearchParams(window.location.search);
	if (directParams.has("id")) {
		return directParams;
	}
	const page = directParams.get("page") || "";
	const split = page.split("?");
	return new URLSearchParams(split[1] || "");
};

// ============================================
// PNP CONFIRM MODAL - Global Function
// ============================================
var PnpConfirmModal = (function () {
	var overlay = null;
	var isOpen = false;

	var ICONS = {
		confirm: '<span style="font-size:38px;font-weight:500;line-height:1;">?</span>',
		danger: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>',
		success: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>',
		warning: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>'
	};

	function createModal() {
		if (overlay) return;
		overlay = document.createElement('div');
		overlay.className = 'pnp-confirm-overlay';
		overlay.setAttribute('role', 'dialog');
		overlay.setAttribute('aria-modal', 'true');
		overlay.innerHTML =
			'<div class="pnp-confirm-card">' +
			'<div class="pnp-confirm-body">' +
			'<div class="pnp-confirm-icon primary" id="pnpConfirmIcon"></div>' +
			'<h3 class="pnp-confirm-title" id="pnpConfirmTitle">Konfirmasi</h3>' +
			'<p class="pnp-confirm-message" id="pnpConfirmMessage"></p>' +
			'</div>' +
			'<div class="pnp-confirm-footer">' +
			'<button type="button" class="pnp-confirm-btn pnp-confirm-btn-cancel" id="pnpConfirmCancel">Batal</button>' +
			'<button type="button" class="pnp-confirm-btn pnp-confirm-btn-confirm" id="pnpConfirmConfirm">Ya, Lanjutkan</button>' +
			'</div>' +
			'</div>';
		document.body.appendChild(overlay);
	}

	function getVariant(options) {
		var variant = options.variant || 'primary';
		var allowed = ['primary', 'danger', 'success', 'warning'];
		return allowed.indexOf(variant) !== -1 ? variant : 'primary';
	}

	function showConfirmModal(callback, options) {
		options = options || {};
		createModal();
		if (isOpen) return;
		isOpen = true;

		var title = options.title || 'Konfirmasi';
		var message = options.message || 'Apakah Anda yakin ingin melanjutkan tindakan ini?';
		var confirmText = options.confirmText || 'Ya, Lanjutkan';
		var cancelText = options.cancelText || 'Batal';
		var variant = getVariant(options);
		var closeOnOverlay = options.closeOnOverlay !== false;

		var iconEl = document.getElementById('pnpConfirmIcon');
		var titleEl = document.getElementById('pnpConfirmTitle');
		var messageEl = document.getElementById('pnpConfirmMessage');
		var confirmBtn = document.getElementById('pnpConfirmConfirm');
		var cancelBtn = document.getElementById('pnpConfirmCancel');

		iconEl.className = 'pnp-confirm-icon ' + variant;
		iconEl.innerHTML = ICONS[variant] || ICONS.confirm;
		titleEl.textContent = title;
		messageEl.textContent = message;
		confirmBtn.textContent = confirmText;
		cancelBtn.textContent = cancelText;
		confirmBtn.className = 'pnp-confirm-btn pnp-confirm-btn-confirm ' + variant;
		confirmBtn.disabled = false;

		overlay.classList.add('show');
		document.body.style.overflow = 'hidden';

		setTimeout(function () {
			confirmBtn.focus();
		}, 200);

		function handleConfirm() {
			if (options.showLoading) {
				confirmBtn.disabled = true;
				confirmBtn.innerHTML = '<span class="pnp-confirm-spinner"></span> Memproses...';
			}
			if (typeof callback === 'function') {
				try {
					callback();
				} catch (e) {
					console.error('Callback error:', e);
				}
			}
			close();
		}

		function handleCancel() {
			if (typeof options.onCancel === 'function') {
				options.onCancel();
			}
			close();
		}

		function handleOverlayClick(e) {
			if (closeOnOverlay && e.target === overlay) {
				handleCancel();
			}
		}

		function handleEscKey(e) {
			if (e.key === 'Escape') handleCancel();
		}

		confirmBtn.onclick = handleConfirm;
		cancelBtn.onclick = handleCancel;
		overlay.onclick = handleOverlayClick;
		document.addEventListener('keydown', handleEscKey);

		function close() {
			overlay.classList.remove('show');
			document.body.style.overflow = '';
			isOpen = false;
			document.removeEventListener('keydown', handleEscKey);
			setTimeout(function () {
				confirmBtn.disabled = false;
				confirmBtn.textContent = confirmText;
			}, 300);
		}
	}

	return {
		show: showConfirmModal
	};
})();

// ============================================
// GLOBAL WRAPPER (Backward Compatible)
// ============================================
function showConfirmModal(callback, options) {
	PnpConfirmModal.show(callback, options);
}

window.goToAbsolute = function (path) {
	window.location.href = path.startsWith("/") ? path : "/" + path;
};

// ============================================
// ONCLICK INPUT GLOBAL DATE/MONTH PICKER
// ============================================
document.addEventListener("pointerdown", function (event) {
	const input = event.target.closest('input[type="date"], input[type="month"]');
	if (!input || input.disabled || input.readOnly) return;
	if (typeof input.showPicker === "function") {
		try { input.showPicker(); } catch (e) { /* biarkan default browser */ }
	}
});

// ============================================
// MONTH PICKER COMPONENT — cross-browser reusable
// ============================================
window.initFlatpickr = function (selector, options) {
	if (typeof flatpickr === 'undefined') {
		console.warn('Flatpickr belum dimuat. Pastikan CDN Flatpickr sudah di-include.');
		return null;
	}
	var defaults = {
		allowInput: true,
		disableMobile: true,
		clickOpens: true
	};
	var config = Object.assign({}, defaults, options || {});
	return flatpickr(selector, config);
};

document.addEventListener('DOMContentLoaded', function () {
	if (typeof flatpickr !== 'undefined' && typeof monthSelectPlugin !== 'undefined') {
		flatpickr('.flatpickr-month', {
			plugins: [new monthSelectPlugin({
				shorthand: false,
				dateFormat: "Y-m",
				altFormat: "F Y"
			})],
			allowInput: true,
			disableMobile: true
		});
	}
});

document.addEventListener("input", function (event) {
    const fieldIds = [
        "namaAgenda",
        "periodeBerjalan",
        "tanggalMulai",
        "tanggalSelesai",
        "durasi"
    ];
    if (fieldIds.includes(event.target.id)) {
        if (typeof PnpValidator !== "undefined" && typeof PnpValidator.clearFieldError === "function") {
            PnpValidator.clearFieldError(event.target);
        }
    }
});

document.addEventListener("change", function (event) {
    const fieldIds = [
        "periodeBerjalan",
        "tanggalMulai",
        "tanggalSelesai",
        "kelompokDistribusi",
        "filterCreatedFrom",
        "filterCreatedTo",
        "filterUpdatedFrom",
        "filterUpdatedTo"
    ];
    if (fieldIds.includes(event.target.id)) {
        if (typeof PnpValidator !== "undefined" && typeof PnpValidator.clearFieldError === "function") {
            PnpValidator.clearFieldError(event.target);
        }
    }
});

function formatDate(dateValue, withTime = false) {
	if (!dateValue) return "-";

	try {
		let date;

		if (typeof dateValue === "string") {
			const match = dateValue.match(/\/Date\((\d+)\)\//);

			if (match) {
				date = new Date(parseInt(match[1], 10));
			} else {
				date = new Date(dateValue);
			}
		} else {
			date = new Date(dateValue);
		}

		if (isNaN(date.getTime())) {
			return "-";
		}

		const options = {
			day: "2-digit",
			month: "long",
			year: "numeric"
		};

		if (withTime) {
			options.hour = "2-digit";
			options.minute = "2-digit";
			options.second = "2-digit";
			options.hour12 = false;
		}

		return date.toLocaleString("id-ID", options);
	} catch (err) {
		console.error("formatDate error:", err, dateValue);
		return "-";
	}
}

function initDateRange(fromId, toId) {
    var fromEl = document.getElementById(fromId);
    var toEl = document.getElementById(toId);

    if (!fromEl || !toEl) {
        return [];
    }

    var fpTo = flatpickr(toEl, {
        dateFormat: "Y-m-d",
        allowInput: true,
        disableMobile: true,

        onChange: function (selectedDates) {

            if (
                selectedDates.length &&
                fpFrom.selectedDates.length &&
                selectedDates[0] < fpFrom.selectedDates[0]
            ) {
                fpTo.clear();

                showToast(
                    "Tanggal akhir tidak boleh lebih kecil dari tanggal awal.",
                    false
                );
            }
        }
    });

    var fpFrom = flatpickr(fromEl, {
        dateFormat: "Y-m-d",
        allowInput: true,
        disableMobile: true,

        onChange: function (selectedDates) {

            if (selectedDates.length > 0) {

                fpTo.set("minDate", selectedDates[0]);

                if (
                    fpTo.selectedDates.length > 0 &&
                    fpTo.selectedDates[0] < selectedDates[0]
                ) {
                    fpTo.clear();
                }

            } else {

                fpTo.set("minDate", null);

            }
        }
    });

    if (fromEl.value) {
        fpTo.set("minDate", fromEl.value);
    }

    return [fpFrom, fpTo];
}

function resetDateRange(fromId, toId) {

    var fromEl = document.getElementById(fromId);
    var toEl = document.getElementById(toId);

    if (fromEl && fromEl._flatpickr) {
        fromEl._flatpickr.clear();
    }

    if (toEl && toEl._flatpickr) {
        toEl._flatpickr.clear();
        toEl._flatpickr.set("minDate", null);
    }

    if (fromEl) {
        fromEl.value = "";
    }

    if (toEl) {
        toEl.value = "";
    }
}
← Back to timeline