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

Survey | JustPaste.app
22 days ago1 views
👨‍💻Programming

Survey

-- Migrasi: target user untuk Agenda Survey Non PNP
-- Data lama otomatis menjadi 'All' (Semua User), jadi perilaku lama tidak berubah.

IF COL_LENGTH('dbo.ISEE_PNP_AgendaSurveyNonPNP', 'TujuanType') IS NULL
    ALTER TABLE dbo.ISEE_PNP_AgendaSurveyNonPNP
        ADD TujuanType NVARCHAR(30) NOT NULL
            CONSTRAINT DF_AgendaSurveyNonPNP_TujuanType DEFAULT 'All';
GO

IF COL_LENGTH('dbo.ISEE_PNP_AgendaSurveyNonPNP', 'TargetPosition') IS NULL
    ALTER TABLE dbo.ISEE_PNP_AgendaSurveyNonPNP ADD TargetPosition NVARCHAR(MAX) NULL;
GO

IF COL_LENGTH('dbo.ISEE_PNP_AgendaSurveyNonPNP', 'TargetJobTitle') IS NULL
    ALTER TABLE dbo.ISEE_PNP_AgendaSurveyNonPNP ADD TargetJobTitle NVARCHAR(MAX) NULL;
GO

IF OBJECT_ID('dbo.ISEE_PNP_AgendaSurveyNonPNPTarget', 'U') IS NULL
BEGIN
    CREATE TABLE dbo.ISEE_PNP_AgendaSurveyNonPNPTarget
    (
        ID             INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
        AgendaNonPNPID INT NOT NULL,
        DomainID       NVARCHAR(100) NOT NULL
    );

    CREATE INDEX IX_AgendaSurveyNonPNPTarget_Agenda
        ON dbo.ISEE_PNP_AgendaSurveyNonPNPTarget (AgendaNonPNPID, DomainID);
END
GO



















# Patch: Target User pada Agenda Survey Non PNP

Urutan pengerjaan: jalankan SQL `01-migrasi-target-survey-nonpnp.sql` dulu, lalu terapkan patch di bawah.
Pola target meniru Running Text: `All` (Semua User), `PositionJobTitle` (cocok Position DAN Job Title), `Manual` (daftar Domain ID).
Data lama otomatis `All`, jadi survey yang sudah ada tetap tampil untuk semua user.

File yang berubah: ParameterSurveyModels.cs, ParameterSurveyRepository.cs, parameter-survey.html, parameter-survey.js, UjianPesertaRepository.cs, UjianPesertaHandler.ashx.
ParameterSurveyHandler.ashx dan ujian-peserta.js tidak perlu diubah.

---

## 1. ParameterSurveyModels.cs

Ganti class `AgendaSurveyNonPNPModel` dan `SaveAgendaNonPNPRequest`:

```csharp
public class AgendaSurveyNonPNPModel
{
    public int ID { get; set; }
    public string NamaAgenda { get; set; }
    public int PertanyaanID { get; set; }
    public string NamaTemplate { get; set; }
    public string StartDate { get; set; }
    public string EndDate { get; set; }
    public string TujuanType { get; set; }
    public string TujuanDisplay { get; set; }
    public List<string> TargetPositions { get; set; }
    public List<string> TargetJobTitles { get; set; }
    public List<string> ManualDomainIds { get; set; }
    public List<PertanyaanTemplateModel> DaftarPertanyaan { get; set; }
    public AgendaSurveyNonPNPModel()
    {
        TujuanType = "All";
        TargetPositions = new List<string>();
        TargetJobTitles = new List<string>();
        ManualDomainIds = new List<string>();
        DaftarPertanyaan = new List<PertanyaanTemplateModel>();
    }
}

public class SaveAgendaNonPNPRequest
{
    public int ID { get; set; }
    public string NamaAgenda { get; set; }
    public int PertanyaanID { get; set; }
    public string StartDate { get; set; }
    public string EndDate { get; set; }
    public string TujuanType { get; set; }
    public List<string> TargetPositions { get; set; }
    public List<string> TargetJobTitles { get; set; }
    public List<string> ManualDomainIds { get; set; }
    public SaveAgendaNonPNPRequest()
    {
        TujuanType = "All";
        TargetPositions = new List<string>();
        TargetJobTitles = new List<string>();
        ManualDomainIds = new List<string>();
    }
}
```

---

## 2. ParameterSurveyRepository.cs

Tambahkan `using System.Linq;` di bagian atas.

### 2a. Helper baru (taruh dekat `GetSafeUserId`)

```csharp
private const string TARGET_DELIMITER = "|";

private static string NormalizeTujuanType(string type)
{
    return (type == "PositionJobTitle" || type == "Manual") ? type : "All";
}

private static string JoinTargets(List<string> items)
{
    if (items == null) return null;
    var clean = items
        .Where(x => !string.IsNullOrWhiteSpace(x))
        .Select(x => x.Trim().Replace(TARGET_DELIMITER, " "))
        .Distinct(StringComparer.OrdinalIgnoreCase)
        .ToList();
    return clean.Count == 0 ? null : string.Join(TARGET_DELIMITER, clean);
}

private static List<string> SplitTargets(string value)
{
    if (string.IsNullOrWhiteSpace(value)) return new List<string>();
    return value.Split(new[] { TARGET_DELIMITER[0] })
                .Select(x => x.Trim())
                .Where(x => x.Length > 0)
                .ToList();
}

private static string BuildTujuanDisplay(
    string type, List<string> positions, List<string> jobTitles, int manualCount)
{
    if (type == "PositionJobTitle")
    {
        return "Position: " + (positions.Count > 0 ? string.Join(", ", positions) : "-")
             + " / Job Title: " + (jobTitles.Count > 0 ? string.Join(", ", jobTitles) : "-");
    }
    if (type == "Manual") return "Manual (" + manualCount + " user)";
    return "Semua User";
}

private static void ReplaceManualTargetsInTransaction(
    SqlConnection conn, SqlTransaction trans, int agendaId, List<string> domainIds)
{
    using (var del = new SqlCommand(
        "DELETE FROM ISEE_PNP_AgendaSurveyNonPNPTarget WHERE AgendaNonPNPID = @id", conn, trans))
    {
        del.Parameters.Add("@id", SqlDbType.Int).Value = agendaId;
        del.ExecuteNonQuery();
    }

    foreach (var domainId in domainIds)
    {
        using (var ins = new SqlCommand(
            "INSERT INTO ISEE_PNP_AgendaSurveyNonPNPTarget (AgendaNonPNPID, DomainID) VALUES (@id, @did)",
            conn, trans))
        {
            ins.Parameters.Add("@id", SqlDbType.Int).Value = agendaId;
            ins.Parameters.Add("@did", SqlDbType.NVarChar, 100).Value = domainId;
            ins.ExecuteNonQuery();
        }
    }
}
```

### 2b. Ganti seluruh method `GetAgendaNonPNPList`

```csharp
public List<AgendaSurveyNonPNPModel> GetAgendaNonPNPList()
{
    var list = new List<AgendaSurveyNonPNPModel>();

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

        const string query = @"
            SELECT
                agenda.ID,
                agenda.NamaAgenda,
                agenda.PertanyaanID,
                agenda.StartDate,
                agenda.EndDate,
                template.NamaPertanyaan,
                ISNULL(agenda.TujuanType, 'All'),
                agenda.TargetPosition,
                agenda.TargetJobTitle
            FROM ISEE_PNP_AgendaSurveyNonPNP agenda
            LEFT JOIN ISEE_PNP_PertanyaanSurvey template
                ON agenda.PertanyaanID = template.ID
            ORDER BY agenda.ID DESC";

        using (var cmd = new SqlCommand(query, conn))
        using (var reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                list.Add(new AgendaSurveyNonPNPModel
                {
                    ID = reader.GetInt32(0),
                    NamaAgenda = reader.IsDBNull(1) ? "" : Convert.ToString(reader.GetValue(1)),
                    PertanyaanID = reader.IsDBNull(2) ? 0 : Convert.ToInt32(reader.GetValue(2)),
                    StartDate = reader.IsDBNull(3) ? "" : Convert.ToDateTime(reader.GetValue(3)).ToString("yyyy-MM-dd"),
                    EndDate = reader.IsDBNull(4) ? "" : Convert.ToDateTime(reader.GetValue(4)).ToString("yyyy-MM-dd"),
                    NamaTemplate = reader.IsDBNull(5) ? "" : Convert.ToString(reader.GetValue(5)),
                    TujuanType = NormalizeTujuanType(Convert.ToString(reader.GetValue(6))),
                    TargetPositions = SplitTargets(reader.IsDBNull(7) ? null : Convert.ToString(reader.GetValue(7))),
                    TargetJobTitles = SplitTargets(reader.IsDBNull(8) ? null : Convert.ToString(reader.GetValue(8)))
                });
            }
        }

        // Target manual (reader sebelumnya sudah ditutup)
        var byId = list.ToDictionary(x => x.ID);
        using (var cmdTarget = new SqlCommand(
            "SELECT AgendaNonPNPID, DomainID FROM ISEE_PNP_AgendaSurveyNonPNPTarget ORDER BY ID", conn))
        using (var rTarget = cmdTarget.ExecuteReader())
        {
            while (rTarget.Read())
            {
                AgendaSurveyNonPNPModel model;
                if (byId.TryGetValue(rTarget.GetInt32(0), out model) && !rTarget.IsDBNull(1))
                    model.ManualDomainIds.Add(rTarget.GetString(1));
            }
        }

        foreach (var agenda in list)
        {
            agenda.TujuanDisplay = BuildTujuanDisplay(
                agenda.TujuanType, agenda.TargetPositions, agenda.TargetJobTitles, agenda.ManualDomainIds.Count);

            agenda.DaftarPertanyaan = agenda.PertanyaanID > 0
                ? GetPertanyaanByTemplateID(conn, agenda.PertanyaanID)
                : new List<PertanyaanTemplateModel>();
        }
    }

    return list;
}
```

### 2c. Ganti seluruh method `SaveAgendaNonPNP`

```csharp
public int SaveAgendaNonPNP(SaveAgendaNonPNPRequest req, string userId)
{
    if (req == null) throw new ArgumentNullException("req");
    if (string.IsNullOrWhiteSpace(req.NamaAgenda)) throw new Exception("Nama agenda wajib diisi.");
    if (req.PertanyaanID <= 0) throw new Exception("Template pertanyaan wajib dipilih.");

    DateTime startDate, endDate;
    if (!DateTime.TryParse(req.StartDate, out startDate)) throw new Exception("Format tanggal mulai tidak valid.");
    if (!DateTime.TryParse(req.EndDate, out endDate)) throw new Exception("Format tanggal selesai tidak valid.");
    if (endDate < startDate) throw new Exception("Tanggal selesai tidak boleh lebih kecil dari tanggal mulai.");

    // ---- Validasi target user ----
    string tujuanType = NormalizeTujuanType(req.TujuanType);
    string posJoined = null, jobJoined = null;
    var manualIds = new List<string>();

    if (tujuanType == "PositionJobTitle")
    {
        posJoined = JoinTargets(req.TargetPositions);
        jobJoined = JoinTargets(req.TargetJobTitles);
        if (posJoined == null) throw new Exception("Pilih minimal 1 Position.");
        if (jobJoined == null) throw new Exception("Pilih minimal 1 Job Title.");
    }
    else if (tujuanType == "Manual")
    {
        if (req.ManualDomainIds != null)
        {
            manualIds = req.ManualDomainIds
                .Where(x => !string.IsNullOrWhiteSpace(x))
                .Select(x => x.Trim())
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList();
        }
        if (manualIds.Count == 0) throw new Exception("Pilih minimal 1 user.");
    }

    string tujuanLog = BuildTujuanDisplay(
        tujuanType, SplitTargets(posJoined), SplitTargets(jobJoined), manualIds.Count);

    string domainId = GetSafeUserId(userId);

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

        using (var trans = conn.BeginTransaction())
        {
            try
            {
                string templateName = GetTemplateNameInTransaction(conn, trans, req.PertanyaanID, true);
                int agendaId;

                if (req.ID == 0)
                {
                    const string insertQuery = @"
                        INSERT INTO ISEE_PNP_AgendaSurveyNonPNP
                        (NamaAgenda, PertanyaanID, StartDate, EndDate,
                         TujuanType, TargetPosition, TargetJobTitle, CreatedBy, CreatedAt)
                        VALUES
                        (@namaAgenda, @pertanyaanId, @startDate, @endDate,
                         @tujuanType, @pos, @job, @userId, GETDATE());
                        SELECT CAST(SCOPE_IDENTITY() AS INT);";

                    using (var cmdInsert = new SqlCommand(insertQuery, conn, trans))
                    {
                        cmdInsert.Parameters.Add("@namaAgenda", SqlDbType.NVarChar, 500).Value = req.NamaAgenda.Trim();
                        cmdInsert.Parameters.Add("@pertanyaanId", SqlDbType.Int).Value = req.PertanyaanID;
                        cmdInsert.Parameters.Add("@startDate", SqlDbType.DateTime).Value = startDate;
                        cmdInsert.Parameters.Add("@endDate", SqlDbType.DateTime).Value = endDate;
                        cmdInsert.Parameters.Add("@tujuanType", SqlDbType.NVarChar, 30).Value = tujuanType;
                        cmdInsert.Parameters.Add("@pos", SqlDbType.NVarChar, -1).Value = (object)posJoined ?? DBNull.Value;
                        cmdInsert.Parameters.Add("@job", SqlDbType.NVarChar, -1).Value = (object)jobJoined ?? DBNull.Value;
                        cmdInsert.Parameters.Add("@userId", SqlDbType.NVarChar, 100).Value = domainId;
                        agendaId = Convert.ToInt32(cmdInsert.ExecuteScalar());
                    }

                    InsertHistoryLogInTransaction(conn, trans, domainId,
                        string.Format(
                            "Menambahkan Agenda Survey Non PNP '{0}' dengan Template '{1}', Tujuan '{2}'",
                            req.NamaAgenda, templateName, tujuanLog),
                        "AgendaSurveyNonPNP", agendaId.ToString());
                }
                else
                {
                    string oldAgendaName = "";
                    string oldTemplateName = "";

                    const string oldQuery = @"
                        SELECT agenda.NamaAgenda, template.NamaPertanyaan
                        FROM ISEE_PNP_AgendaSurveyNonPNP agenda
                        LEFT JOIN ISEE_PNP_PertanyaanSurvey template
                            ON agenda.PertanyaanID = template.ID
                        WHERE agenda.ID = @id";

                    using (var cmdOld = new SqlCommand(oldQuery, conn, trans))
                    {
                        cmdOld.Parameters.Add("@id", SqlDbType.Int).Value = req.ID;
                        using (var reader = cmdOld.ExecuteReader())
                        {
                            if (!reader.Read()) throw new Exception("Agenda survey Non PNP tidak ditemukan.");
                            oldAgendaName = reader.IsDBNull(0) ? "" : Convert.ToString(reader.GetValue(0));
                            oldTemplateName = reader.IsDBNull(1) ? "" : Convert.ToString(reader.GetValue(1));
                        }
                    }

                    agendaId = req.ID;

                    const string updateQuery = @"
                        UPDATE ISEE_PNP_AgendaSurveyNonPNP
                        SET NamaAgenda = @namaAgenda,
                            PertanyaanID = @pertanyaanId,
                            StartDate = @startDate,
                            EndDate = @endDate,
                            TujuanType = @tujuanType,
                            TargetPosition = @pos,
                            TargetJobTitle = @job,
                            UpdatedBy = @userId,
                            UpdatedAt = GETDATE()
                        WHERE ID = @id";

                    using (var cmdUpdate = new SqlCommand(updateQuery, conn, trans))
                    {
                        cmdUpdate.Parameters.Add("@namaAgenda", SqlDbType.NVarChar, 500).Value = req.NamaAgenda.Trim();
                        cmdUpdate.Parameters.Add("@pertanyaanId", SqlDbType.Int).Value = req.PertanyaanID;
                        cmdUpdate.Parameters.Add("@startDate", SqlDbType.DateTime).Value = startDate;
                        cmdUpdate.Parameters.Add("@endDate", SqlDbType.DateTime).Value = endDate;
                        cmdUpdate.Parameters.Add("@tujuanType", SqlDbType.NVarChar, 30).Value = tujuanType;
                        cmdUpdate.Parameters.Add("@pos", SqlDbType.NVarChar, -1).Value = (object)posJoined ?? DBNull.Value;
                        cmdUpdate.Parameters.Add("@job", SqlDbType.NVarChar, -1).Value = (object)jobJoined ?? DBNull.Value;
                        cmdUpdate.Parameters.Add("@userId", SqlDbType.NVarChar, 100).Value = domainId;
                        cmdUpdate.Parameters.Add("@id", SqlDbType.Int).Value = req.ID;

                        if (cmdUpdate.ExecuteNonQuery() == 0)
                            throw new Exception("Agenda survey Non PNP gagal diperbarui.");
                    }

                    InsertHistoryLogInTransaction(conn, trans, domainId,
                        string.Format(
                            "Mengubah Agenda Survey Non PNP dari '{0}' menjadi '{1}', Template dari '{2}' menjadi '{3}', Tujuan '{4}'",
                            oldAgendaName, req.NamaAgenda, oldTemplateName, templateName, tujuanLog),
                        "AgendaSurveyNonPNP", req.ID.ToString());
                }

                // Target manual: selalu disinkronkan (dikosongkan bila bukan Manual)
                ReplaceManualTargetsInTransaction(conn, trans, agendaId, manualIds);

                trans.Commit();
                return agendaId;
            }
            catch
            {
                RollbackTransaction(trans);
                throw;
            }
        }
    }
}
```

### 2d. `DeleteAgendaNonPNP`

Di dalam transaksi, sebelum `DELETE FROM ISEE_PNP_AgendaSurveyNonPNP`, tambahkan:

```csharp
using (var cmdTarget = new SqlCommand(
    "DELETE FROM ISEE_PNP_AgendaSurveyNonPNPTarget WHERE AgendaNonPNPID = @id", conn, trans))
{
    cmdTarget.Parameters.Add("@id", SqlDbType.Int).Value = id;
    cmdTarget.ExecuteNonQuery();
}
```

---

## 3. UjianPesertaRepository.cs (sisi peserta: ini yang membuat data survey sesuai target)

### 3a. Konstanta filter (taruh di bawah `SqlRemaining`)

```csharp
// Filter target survey Non PNP. Alias tabel agenda harus "a"; wajib ada parameter @domainId.
private const string SqlNonPnpTargetFilter = @"
	(
		ISNULL(a.TujuanType, 'All') = 'All'
		OR (a.TujuanType = 'Manual' AND EXISTS (
			SELECT 1 FROM ISEE_PNP_AgendaSurveyNonPNPTarget t
			WHERE t.AgendaNonPNPID = a.ID AND t.DomainID = @domainId))
		OR (a.TujuanType = 'PositionJobTitle' AND EXISTS (
			SELECT 1 FROM v_ISEE_STAFF_DATA s
			WHERE s.Domain_ID = @domainId
			  AND CHARINDEX('|' + LTRIM(RTRIM(s.Position)) + '|', '|' + ISNULL(a.TargetPosition, '') + '|') > 0
			  AND CHARINDEX('|' + LTRIM(RTRIM(s.Job_Title)) + '|', '|' + ISNULL(a.TargetJobTitle, '') + '|') > 0))
	)";
```

### 3b. `GetNonPNPSurveyDashboard`: ganti pembuatan `cmd` pertama

```csharp
var cmd = new SqlCommand(@"SELECT a.ID, a.NamaAgenda, a.StartDate, a.EndDate, a.PertanyaanID
	FROM ISEE_PNP_AgendaSurveyNonPNP a
	WHERE " + SqlNonPnpTargetFilter + @"
	ORDER BY a.StartDate DESC", conn);
cmd.Parameters.AddWithValue("@domainId", domainId);
```

### 3c. `GetSurveyNonPNPByID`: tambah parameter `domainId` dan filter akses

Signature baru: `public SurveyDataModel GetSurveyNonPNPByID(int agendaNonPnpId, string domainId)`, tambahkan `domainId = RequireDomain(domainId);` di baris pertama, lalu ganti query pertamanya:

```csharp
using (var cmd = new SqlCommand(
	"SELECT a.ID, a.NamaAgenda, a.PertanyaanID FROM ISEE_PNP_AgendaSurveyNonPNP a WHERE a.ID = @id AND " + SqlNonPnpTargetFilter, conn))
{
	cmd.Parameters.AddWithValue("@id", agendaNonPnpId);
	cmd.Parameters.AddWithValue("@domainId", domainId);
	// ... sisanya tetap
```

Bila user bukan target, `model` tetap `null` dan front-end menampilkan "Survey tidak ditemukan.".

### 3d. `SaveSurveyAnswer`: blokir jawaban dari user yang bukan target

Setelah blok cek kepemilikan `ujianPesertaId` (di dalam `try`, sebelum `object uid = ...`), tambahkan:

```csharp
if (agendaNonPnpId.HasValue && agendaNonPnpId.Value > 0)
{
	using (var cmdAcc = new SqlCommand(
		"SELECT COUNT(*) FROM ISEE_PNP_AgendaSurveyNonPNP a WHERE a.ID = @anpid AND " + SqlNonPnpTargetFilter, conn, trans))
	{
		cmdAcc.Parameters.AddWithValue("@anpid", agendaNonPnpId.Value);
		cmdAcc.Parameters.AddWithValue("@domainId", domainId);
		if (Convert.ToInt32(cmdAcc.ExecuteScalar()) == 0)
			throw new ExamException("Survey ini tidak ditujukan untuk Anda.", 403);
	}
}
```

---

## 4. UjianPesertaHandler.ashx

```csharp
case "GetNonPNPSurveyByID":
    result = repo.GetSurveyNonPNPByID(Convert.ToInt32(context.Request["id"]), domainId);
    break;
```

---

## 5. parameter-survey.html

### 5a. Tambahkan CSS (di akhir blok `<style>`)

```css
/* Target user Non PNP */
.field-hint { display: block; margin-top: 4px; color: var(--ink-400, #94A3B8); font-size: 12px; }
.field-error { margin-top: 4px; color: var(--danger, #DC2626); font-size: 12px; }
select.input-error + .select2-container .select2-selection { border-color: var(--danger, #DC2626) !important; }

#nonpnp-modal .modal-body { overflow-x: hidden; }
#nonpnp-modal .select2-container { display: block; width: 100% !important; max-width: 100% !important; }
#nonpnp-modal .select2-container--default .select2-selection--multiple {
    min-height: 42px; max-height: 120px; padding: 4px 8px;
    overflow-y: auto; overflow-x: hidden; box-sizing: border-box;
}
#nonpnp-modal .select2-selection--multiple .select2-selection__rendered {
    display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin: 0; padding: 0; white-space: normal;
}
#nonpnp-modal .select2-selection--multiple .select2-selection__choice {
    margin: 0 !important; max-width: calc(100% - 8px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#nonpnp-modal .select2-selection--multiple .select2-search--inline { flex: 1 1 120px; min-width: 120px; margin: 0; padding: 0; }
#nonpnp-modal .select2-selection--multiple .select2-search__field {
    margin: 0 !important; padding: 0 !important; height: 30px !important; line-height: 30px !important; resize: none !important;
}
```

### 5b. Tabel Non PNP: tambah kolom Tujuan

Di `<thead>` tabel `#nonpnp-table`, setelah `<th>Template Survey</th>`:

```html
<th style="width:220px">Tujuan</th>
```

Ubah `colspan="7"` pada baris loading tabel itu menjadi `colspan="8"`.

### 5c. Modal `#nonpnp-modal`: tambahkan blok Tujuan

Letakkan tepat setelah `</div>` penutup `<div class="form-grid" ...>` (Start Date / End Date), sebelum `</div>` penutup `modal-body`:

```html
<div class="form-group">
    <label>Tujuan <span style="color:var(--danger)">*</span></label>
    <select id="nonpnp-tujuan-type" class="select2">
        <option value="All">Semua User</option>
        <option value="PositionJobTitle">Berdasarkan Position &amp; Job Title</option>
        <option value="Manual">Pilih User Manual</option>
    </select>
</div>

<div id="nonpnp-tujuan-posjob-wrap" style="display:none;">
    <div class="form-grid" style="grid-template-columns:1fr 1fr;">
        <div class="form-group">
            <label>Position <span style="color:var(--danger)">*</span></label>
            <select id="nonpnp-position" class="select2" multiple="multiple"></select>
            <small class="field-hint">Bisa pilih lebih dari satu. User harus cocok Position DAN Job Title.</small>
        </div>
        <div class="form-group">
            <label>Job Title <span style="color:var(--danger)">*</span></label>
            <select id="nonpnp-jobtitle" class="select2" multiple="multiple"></select>
            <small class="field-hint">Bisa pilih lebih dari satu.</small>
        </div>
    </div>
</div>

<div id="nonpnp-tujuan-manual-wrap" style="display:none;">
    <div class="form-group">
        <label>Pilih User <span style="color:var(--danger)">*</span></label>
        <select id="nonpnp-manual-users" class="select2" multiple="multiple"></select>
        <small class="field-hint">Bisa pilih lebih dari satu user.</small>
    </div>
</div>
```

---

## 6. parameter-survey.js

### 6a. Konstanta (di bawah `SV_TTL_DROPDOWN`)

```js
var SV_NOTIF_URL = "/Handlers/pnp/notifikasi/PengaturanNotifikasiHandler.ashx";
var SV_STAFF_URL = "/Handlers/pnp/distribusi-soal/DistribusiSoalHandler.ashx";
```

### 6b. Properti `SurveyApp`

Tambahkan: `_targetDropdownPromise: null,`

Di `init`, setelah `this._initRealtime();` tambahkan:

```js
this._targetDropdownPromise = this._loadTargetDropdowns(token, false);
```

Di `cleanup`, tambahkan: `this._targetDropdownPromise = null;`

### 6c. Tabel Non PNP: kolom Tujuan

`_setupNonPNPTable`: ubah `colspan: 7` menjadi `colspan: 8`.

Di `_renderNonPNPRow`, setelah baris `<td>` Template (yang berisi `item.NamaTemplate`) tambahkan:

```js
html += '<td style="white-space:normal;">' + this._esc(item.TujuanDisplay || "Semua User") + '</td>';
```

dan ubah `colspan="7"` pada baris `history-row` menjadi `colspan="8"`.

Di `_filterNonPNP`, tambahkan `item.TujuanDisplay` ke array nilai pencarian:

```js
item.StartDate,
item.EndDate,
item.TujuanDisplay
```

### 6d. Ganti `_initModalSelect2`

```js
_initModalSelect2: function (modalEl) {
    var self = this;
    if (typeof $ === "undefined" || !modalEl) return;

    var multiIds = ["nonpnp-position", "nonpnp-jobtitle", "nonpnp-manual-users"];

    $(modalEl).find(".select2").each(function () {
        var $select = $(this);
        if ($select.data("select2")) $select.select2("destroy");

        var options = { width: "100%", dropdownParent: $(modalEl) };

        if (this.id === "nonpnp-tujuan-type") options.minimumResultsForSearch = -1;
        if (this.id === "nonpnp-position") { options.placeholder = "Pilih Position"; options.closeOnSelect = false; }
        if (this.id === "nonpnp-jobtitle") { options.placeholder = "Pilih Job Title"; options.closeOnSelect = false; }
        if (this.id === "nonpnp-manual-users") { options.placeholder = "Pilih user"; options.closeOnSelect = false; }

        $select.select2(options);
        $select.off(".surveyApp");

        if (this.id === "pnp-agenda-distribusi") {
            $select.on("select2:select.surveyApp select2:clear.surveyApp", function () { self._onPNPDistribusiChange(this); });
        }
        if (this.id === "pnp-setting-pertanyaan") {
            $select.on("select2:select.surveyApp select2:clear.surveyApp", function () {
                self._renderTemplatePreview(this.value, "pnp-setting-template-preview", "pnp-setting-preview-group");
            });
        }
        if (this.id === "pnp-pertanyaan") {
            $select.on("select2:select.surveyApp select2:clear.surveyApp", function () {
                self._renderTemplatePreview(this.value, "pnp-template-preview", "pnp-preview-group");
            });
        }
        if (this.id === "nonpnp-pertanyaan") {
            $select.on("select2:select.surveyApp select2:clear.surveyApp", function () {
                self._renderTemplatePreview(this.value, "nonpnp-template-preview", "nonpnp-preview-group");
            });
        }
        if (this.id === "nonpnp-tujuan-type") {
            $select.on("change.surveyApp", function () { self._onNonPNPTujuanChange(); });
        }
        if (multiIds.indexOf(this.id) !== -1) {
            $select.on("change.surveyApp", function () { self._clearFieldError(this); });
        }
    });
},
```

### 6e. Method baru (tambahkan di dalam `SurveyApp`)

```js
_fillOptions: function (id, items, textFn, valueFn) {
    var el = document.getElementById(id);
    if (!el) return;
    var html = "";
    (items || []).forEach(function (it) {
        html += '<option value="' + this._esc(valueFn(it)) + '">' + this._esc(textFn(it)) + "</option>";
    }, this);
    el.innerHTML = html;
},

// Position, Job Title & User: sumber dan cache key sama dengan halaman Running Text / Notifikasi.
_loadTargetDropdowns: function (token, force) {
    var self = this;

    function cached(key, url) {
        var fetcher = function () { return self._svSafeFetch(url); };
        if (typeof PnpCache !== "undefined") {
            return PnpCache.fetchWithCache(key, fetcher, { force: force, ttl: SV_TTL_DROPDOWN });
        }
        return fetcher();
    }

    return Promise.all([
        cached("nf:position", SV_NOTIF_URL + "?action=GetPositionList"),
        cached("nf:jobtitle", SV_NOTIF_URL + "?action=GetJobTitleList"),
        cached("nf:staff", SV_STAFF_URL + "?action=GetStaffList")
    ]).then(function (res) {
        if (token !== self._pageToken) return;
        self._fillOptions("nonpnp-position", res[0], function (i) { return i.Text; }, function (i) { return i.Value; });
        self._fillOptions("nonpnp-jobtitle", res[1], function (i) { return i.Text; }, function (i) { return i.Value; });
        self._fillOptions("nonpnp-manual-users", res[2],
            function (u) { return u.FullName + " (" + u.DomainID + ")"; },
            function (u) { return u.DomainID; });
    }).catch(function (e) {
        console.error("Gagal memuat dropdown target survey:", e);
    });
},

_ensureTargetDropdowns: function () {
    return this._targetDropdownPromise || Promise.resolve();
},

_getMulti: function (id) {
    if (typeof $ === "undefined") return [];
    return $("#" + id).val() || [];
},

_setSelectValue: function (id, value) {
    var el = document.getElementById(id);
    if (!el) return;
    if (typeof $ !== "undefined") $(el).val(value);
    else el.value = value;
},

_clearFieldError: function (el) {
    if (!el) return;
    el.classList.remove("input-error");
    var err = el.parentNode && el.parentNode.querySelector(".field-error");
    if (err) err.remove();
},

_setNonPNPTujuan: function (type, positions, jobTitles, manualIds) {
    this._setSelectValue("nonpnp-tujuan-type", type || "All");
    this._setSelectValue("nonpnp-position", positions || []);
    this._setSelectValue("nonpnp-jobtitle", jobTitles || []);
    this._setSelectValue("nonpnp-manual-users", manualIds || []);
},

_onNonPNPTujuanChange: function () {
    var typeEl = document.getElementById("nonpnp-tujuan-type");
    var posWrap = document.getElementById("nonpnp-tujuan-posjob-wrap");
    var manualWrap = document.getElementById("nonpnp-tujuan-manual-wrap");
    if (!typeEl || !posWrap || !manualWrap) return;

    var type = typeEl.value;
    posWrap.style.display = type === "PositionJobTitle" ? "block" : "none";
    manualWrap.style.display = type === "Manual" ? "block" : "none";

    if (type !== "PositionJobTitle") {
        this._clearFieldError(document.getElementById("nonpnp-position"));
        this._clearFieldError(document.getElementById("nonpnp-jobtitle"));
    }
    if (type !== "Manual") {
        this._clearFieldError(document.getElementById("nonpnp-manual-users"));
    }
},
```

### 6f. Ganti `openNonPNPSetting`

```js
openNonPNPSetting: function () {
    var self = this;

    this._ensureTargetDropdowns().then(function () {
        self.state.editing.nonPnpId = null;

        var titleEl = document.getElementById("nonpnp-modal-title");
        if (titleEl) titleEl.innerHTML = window.pnpIcon("cog", 18) + ' Setting Agenda Survey Non PNP';

        var editIdEl = document.getElementById("nonpnp-edit-id");
        if (editIdEl) editIdEl.value = "";

        var namaEl = document.getElementById("nonpnp-nama-agenda");
        if (namaEl) namaEl.value = "";

        var pertanyaanSel = document.getElementById("nonpnp-pertanyaan");
        if (pertanyaanSel) { pertanyaanSel.value = ""; pertanyaanSel.disabled = false; }

        var previewGroup = document.getElementById("nonpnp-preview-group");
        if (previewGroup) previewGroup.style.display = "none";

        self._setLinkedDateValues("nonPnpModalStart", "nonPnpModalEnd", "", "");
        self._setNonPNPTujuan("All", [], [], []);

        document.querySelectorAll(".nonpnp-edit-hint").forEach(function (el) { el.style.display = "none"; });

        self._clearValidationErrors();
        self._openModal("nonpnp-modal");
        self._onNonPNPTujuanChange();
    });
},
```

### 6g. Ganti `editNonPNP`

```js
editNonPNP: function (id) {
    var self = this;
    var item = this.state.agendaNonPNP.find(function (x) { return x.ID === id; });
    if (!item) return;

    this._ensureTargetDropdowns().then(function () {
        self.state.editing.nonPnpId = id;

        var titleEl = document.getElementById("nonpnp-modal-title");
        if (titleEl) titleEl.innerHTML = window.pnpIcon("edit", 18) + ' Edit Agenda Survey Non PNP';

        var editIdEl = document.getElementById("nonpnp-edit-id");
        if (editIdEl) editIdEl.value = id;

        var namaEl = document.getElementById("nonpnp-nama-agenda");
        if (namaEl) namaEl.value = item.NamaAgenda || "";

        var pertanyaanSel = document.getElementById("nonpnp-pertanyaan");
        if (pertanyaanSel) { pertanyaanSel.value = item.PertanyaanID || ""; pertanyaanSel.disabled = true; }

        self._renderTemplatePreview(item.PertanyaanID, "nonpnp-template-preview", "nonpnp-preview-group");
        self._setLinkedDateValues("nonPnpModalStart", "nonPnpModalEnd", item.StartDate, item.EndDate);
        self._setNonPNPTujuan(item.TujuanType, item.TargetPositions, item.TargetJobTitles, item.ManualDomainIds);

        document.querySelectorAll(".nonpnp-edit-hint").forEach(function (el) { el.style.display = "block"; });

        self._clearValidationErrors();
        self._openModal("nonpnp-modal");
        self._onNonPNPTujuanChange();
    });
},
```

### 6h. `validateNonPNPForm`

Sebelum blok `if (errors.length > 0)`, tambahkan:

```js
var tujuanType = document.getElementById("nonpnp-tujuan-type").value;

if (tujuanType === "PositionJobTitle") {
    var posEl = document.getElementById("nonpnp-position");
    var jobEl = document.getElementById("nonpnp-jobtitle");

    if (this._getMulti("nonpnp-position").length === 0) {
        this._showFieldError(posEl, "Pilih minimal 1 Position");
        errors.push(posEl);
    }
    if (this._getMulti("nonpnp-jobtitle").length === 0) {
        this._showFieldError(jobEl, "Pilih minimal 1 Job Title");
        errors.push(jobEl);
    }
}

if (tujuanType === "Manual") {
    var manualEl = document.getElementById("nonpnp-manual-users");
    if (this._getMulti("nonpnp-manual-users").length === 0) {
        this._showFieldError(manualEl, "Pilih minimal 1 user");
        errors.push(manualEl);
    }
}
```

### 6i. `saveNonPNPAgenda`: ganti pembuatan `payload`

```js
var tujuanType = document.getElementById("nonpnp-tujuan-type").value;

var payload = {
    ID: isEdit ? this.state.editing.nonPnpId : 0,
    NamaAgenda: namaEl ? namaEl.value.trim() : "",
    PertanyaanID: pertanyaanEl ? parseInt(pertanyaanEl.value) || 0 : 0,
    StartDate: startEl ? startEl.value : "",
    EndDate: endEl ? endEl.value : "",
    TujuanType: tujuanType,
    TargetPositions: tujuanType === "PositionJobTitle" ? this._getMulti("nonpnp-position") : [],
    TargetJobTitles: tujuanType === "PositionJobTitle" ? this._getMulti("nonpnp-jobtitle") : [],
    ManualDomainIds: tujuanType === "Manual" ? this._getMulti("nonpnp-manual-users") : []
};
```

Pada callback gagal ganti `showToast("Gagal", false)` menjadi `showToast("Gagal: " + ((res && res.error) || "Error"), false)` supaya pesan validasi server terbaca.

---

## Catatan perilaku

- Position dan Job Title bersifat DAN (sama seperti Running Text): user harus cocok keduanya.
- Pencocokan memakai `v_ISEE_STAFF_DATA` (kolom `Position`, `Job_Title`), sama dengan query dashboard ujian.
- Survey yang sudah pernah diisi akan hilang dari dashboard peserta bila target agenda diubah dan peserta tidak lagi termasuk target.
- Dashboard peserta memuat ulang lewat channel realtime `UjianPeserta`. Bila ingin dashboard peserta ikut ter-refresh saat agenda Non PNP diubah, tambahkan `PnpDataHub.Notify("UjianPeserta");` di case `SaveAgendaNonPNP` dan `DeleteAgendaNonPNP` pada ParameterSurveyHandler.ashx.
← Back to timeline