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

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

pnp 22

berikut data ParameterKelompokDistribusiRepository dan ParameterScoringRepository yang berkaitan dengan member tersebut:

  1. ParameterKelompokDistribusiRepository.cs

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Linq;

public class KelompokDistribusiRepository {
    private string connString = ConfigurationManager.ConnectionStrings["iSee"].ConnectionString;
	
	private string NormalizeText(string value) {
		return (value ?? string.Empty).Trim();
	}

	private string NormalizeInitial(string value) {
		return (value ?? string.Empty).Trim().ToUpper();
	}

    // ==================== GET DATA ====================
    public List<KelompokDistribusiModel> GetAllDistribusi() {
        var list = new List<KelompokDistribusiModel>();
        using (var conn = new SqlConnection(connString)) {
            var cmd = new SqlCommand("SELECT ID, NamaKelompok FROM ISEE_PNP_KelompokDistribusi ORDER BY NamaKelompok", conn);
            conn.Open();
            using (var reader = cmd.ExecuteReader()) {
                while (reader.Read()) {
                    list.Add(new KelompokDistribusiModel {
                        ID = reader.GetInt32(0),
                        NamaKelompok = reader.IsDBNull(1) ? "" : reader.GetString(1)
                    });
                }
            }
        }
        return list;
    }

    public List<string> GetPositions() {
        var list = new List<string>();
        using (var conn = new SqlConnection(connString)) {
            var cmd = new SqlCommand("SELECT [Item] FROM [ISEE_STAFF_PARAM] WHERE [Type] = 'Position' ORDER BY [Item] ASC", conn);
            conn.Open();
            using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { list.Add(reader.GetString(0)); } }
        }
        return list;
    }

    public List<string> GetJobTitles() {
        var list = new List<string>();
        using (var conn = new SqlConnection(connString)) {
            var cmd = new SqlCommand("SELECT [Item] FROM [ISEE_STAFF_PARAM] WHERE [Type] = 'Job Title' ORDER BY [Item] ASC", conn);
            conn.Open();
            using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { list.Add(reader.GetString(0)); } }
        }
        return list;
    }

    public List<MemberDistribusiModel> GetMembers(int distribusiId, string search = "") {
        var list = new List<MemberDistribusiModel>();
        using (var conn = new SqlConnection(connString)) {
            string query = "SELECT ID, KelompokDistribusiID, Skill, Position FROM ISEE_PNP_MemberDistribusi WHERE KelompokDistribusiID = @id";
            if (!string.IsNullOrWhiteSpace(search)) query += " AND (Skill LIKE @search OR Position LIKE @search)";
            var cmd = new SqlCommand(query, conn);
            cmd.Parameters.AddWithValue("@id", distribusiId);
            if (!string.IsNullOrWhiteSpace(search)) cmd.Parameters.AddWithValue("@search", "%" + search + "%");
            conn.Open();
            using (var reader = cmd.ExecuteReader()) {
                while (reader.Read()) {
                    list.Add(new MemberDistribusiModel {
                        ID = reader.GetInt32(0), KelompokDistribusiID = reader.GetInt32(1),
                        Skill = reader.IsDBNull(2) ? "" : reader.GetString(2), 
                        Position = reader.IsDBNull(3) ? "" : reader.GetString(3)
                    });
                }
            }
        }
        return list;
    }

    public List<DistribusiJenisSoalModel> GetJenisSoalByDistribusi(int distribusiId, string search = "") {
        var list = new List<DistribusiJenisSoalModel>();
        using (var conn = new SqlConnection(connString)) {
            string query = @"SELECT a.ID, a.KelompokDistribusiID, a.JenisSoalID, b.NamaJenisSoal, ISNULL(b.Initial1, '') as Initial1, ISNULL(b.Initial2, '') as Initial2
                FROM ISEE_PNP_DistribusiJenisSoal a JOIN ISEE_PNP_JenisSoal b ON a.JenisSoalID = b.ID WHERE a.KelompokDistribusiID = @id";
            if (!string.IsNullOrWhiteSpace(search)) query += " AND (b.NamaJenisSoal LIKE @search OR b.Initial1 LIKE @search OR b.Initial2 LIKE @search)";
            var cmd = new SqlCommand(query, conn);
            cmd.Parameters.AddWithValue("@id", distribusiId);
            if (!string.IsNullOrWhiteSpace(search)) cmd.Parameters.AddWithValue("@search", "%" + search + "%");
            conn.Open();
            using (var reader = cmd.ExecuteReader()) {
                while (reader.Read()) {
                    list.Add(new DistribusiJenisSoalModel {
                        ID = reader.GetInt32(0), KelompokDistribusiID = reader.GetInt32(1), JenisSoalID = reader.GetInt32(2),
                        NamaJenisSoal = reader.IsDBNull(3) ? "" : reader.GetString(3),
                        Initial1 = reader.IsDBNull(4) ? "" : reader.GetString(4).Trim(),
                        Initial2 = reader.IsDBNull(5) ? "" : reader.GetString(5).Trim()
                    });
                }
            }
        }
        return list;
    }

    public List<JenisSoalModel> GetAllJenisSoal() {
        var list = new List<JenisSoalModel>();
        using (var conn = new SqlConnection(connString)) {
            var cmd = new SqlCommand("SELECT ID, NamaJenisSoal, ISNULL(Initial1, '') as Initial1, ISNULL(Initial2, '') as Initial2 FROM ISEE_PNP_JenisSoal ORDER BY NamaJenisSoal", conn);
            conn.Open();
            using (var reader = cmd.ExecuteReader()) {
                while (reader.Read()) {
                    list.Add(new JenisSoalModel {
                        ID = reader.GetInt32(0), NamaJenisSoal = reader.GetString(1),
                        Initial1 = reader.IsDBNull(2) ? "" : reader.GetString(2).Trim(),
                        Initial2 = reader.IsDBNull(3) ? "" : reader.GetString(3).Trim()
                    });
                }
            }
        }
        return list;
    }

    // ==================== HISTORY LOG ====================
    public List<HistoryLogModel> GetLogDistribusi(DateTime? dateFrom, DateTime? dateTo) {
        if (dateFrom.HasValue && dateTo.HasValue && dateFrom.Value > dateTo.Value) {
            throw new ArgumentException("Tanggal awal tidak boleh lebih besar dari tanggal akhir.");
        }

        var list = new List<HistoryLogModel>();
        using (var conn = new SqlConnection(connString)) {
            string query = @"SELECT l.ID, l.CreatedAt, l.DomainID, ISNULL(u.Full_Name, l.DomainID) as UserName, l.Action, l.EntityType, l.EntityID
                FROM ISEE_PNP_HistoryLog l LEFT JOIN ISEE_STAFF_DATA u ON l.DomainID = u.Domain_ID
                WHERE l.EntityType IN ('KelompokDistribusi', 'MemberDistribusi', 'JenisSoalDistribusi')";
            
            if (dateFrom.HasValue) query += " AND l.CreatedAt >= @dateFrom";
            if (dateTo.HasValue) query += " AND l.CreatedAt < @dateTo"; 
            query += " ORDER BY l.CreatedAt DESC";
            
            var cmd = new SqlCommand(query, conn);
            if (dateFrom.HasValue) cmd.Parameters.AddWithValue("@dateFrom", dateFrom.Value);
            if (dateTo.HasValue) cmd.Parameters.AddWithValue("@dateTo", dateTo.Value.Date.AddDays(1));
            
            conn.Open();
            using (var reader = cmd.ExecuteReader()) {
                while (reader.Read()) {
                    list.Add(new HistoryLogModel {
                        ID = reader.GetInt32(0), CreatedAt = reader.GetDateTime(1),
                        DomainID = reader.IsDBNull(2) ? "" : reader.GetString(2),
                        UserName = reader.IsDBNull(3) ? (reader.IsDBNull(2) ? "" : reader.GetString(2)) : reader.GetString(3),
                        Action = reader.IsDBNull(4) ? "" : reader.GetString(4), 
                        EntityType = reader.IsDBNull(5) ? "" : reader.GetString(5), 
                        EntityID = reader.IsDBNull(6) ? "" : reader.GetString(6)
                    });
                }
            }
        }
        return list;
    }

    public List<HistoryLogModel> GetLogJenisSoal(DateTime? dateFrom, DateTime? dateTo) {
		if (dateFrom.HasValue && dateTo.HasValue && dateFrom.Value > dateTo.Value) {
            throw new ArgumentException("Tanggal awal tidak boleh lebih besar dari tanggal akhir.");
        }
		
        var list = new List<HistoryLogModel>();
        using (var conn = new SqlConnection(connString)) {
            string query = @"SELECT l.ID, l.CreatedAt, l.DomainID, ISNULL(u.Full_Name, l.DomainID) as UserName, l.Action, l.EntityType, l.EntityID
                FROM ISEE_PNP_HistoryLog l LEFT JOIN ISEE_STAFF_DATA u ON l.DomainID = u.Domain_ID WHERE l.EntityType = 'JenisSoal'";
			
			if (dateFrom.HasValue) query += " AND l.CreatedAt >= @dateFrom";
            if (dateTo.HasValue) query += " AND l.CreatedAt < @dateTo"; 
            query += " ORDER BY l.CreatedAt DESC";
			
            var cmd = new SqlCommand(query, conn);
            if (dateFrom.HasValue) cmd.Parameters.AddWithValue("@dateFrom", dateFrom.Value);
            if (dateTo.HasValue) cmd.Parameters.AddWithValue("@dateTo", dateTo.Value.AddDays(1).AddSeconds(-1));
            conn.Open();
            using (var reader = cmd.ExecuteReader()) {
                while (reader.Read()) {
					list.Add(new HistoryLogModel {
                        ID = reader.GetInt32(0), CreatedAt = reader.GetDateTime(1),
                        DomainID = reader.IsDBNull(2) ? "" : reader.GetString(2),
                        UserName = reader.IsDBNull(3) ? (reader.IsDBNull(2) ? "" : reader.GetString(2)) : reader.GetString(3),
                        Action = reader.IsDBNull(4) ? "" : reader.GetString(4), 
                        EntityType = reader.IsDBNull(5) ? "" : reader.GetString(5), 
                        EntityID = reader.IsDBNull(6) ? "" : reader.GetString(6)
                    });
                }
            }
        }
        return list;
    }

    private void InsertHistoryLogInTransaction(
		SqlConnection conn,
		SqlTransaction trans,
		string domainID,
		string action,
		string entityType,
		string entityID
	) {
		string query = @"
			INSERT INTO ISEE_PNP_HistoryLog (
				DomainID,
				Action,
				EntityType,
				EntityID,
				CreatedAt
			)
			VALUES (@domainID, @action, @entityType, @entityID, GETDATE())";

		var cmd = new SqlCommand(query, conn, trans);
		cmd.Parameters.AddWithValue("@domainID", domainID);
		cmd.Parameters.AddWithValue("@action", action);
		cmd.Parameters.AddWithValue("@entityType", entityType);
		cmd.Parameters.AddWithValue("@entityID", entityID);
		cmd.ExecuteNonQuery();
	}

    // ==================== SAVE METHODS (FIXED NULL REFERENCE & LOGGING) ====================
    public void SaveDistribusi(List<KelompokDistribusiModel> data, List<int> deletedIds, string userId) {
		data = data ?? new List<KelompokDistribusiModel>();
		deletedIds = deletedIds ?? new List<int>();

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

			using (var trans = conn.BeginTransaction()) {
				try {
					var existingDistribusi = new Dictionary<int, string>();

					var idsToFetch = deletedIds
						.Concat(data.Where(d => d.ID > 0).Select(d => d.ID))
						.Distinct()
						.ToList();

					if (idsToFetch.Count > 0) {
						string idList = string.Join(",", idsToFetch);

						var cmdFetch = new SqlCommand(
							"SELECT ID, NamaKelompok " +
							"FROM ISEE_PNP_KelompokDistribusi " +
							"WHERE ID IN (" + idList + ")",
							conn,
							trans
						);

						using (var reader = cmdFetch.ExecuteReader()) {
							while (reader.Read()) {
								existingDistribusi[reader.GetInt32(0)] = reader.IsDBNull(1)
									? ""
									: reader.GetString(1);
							}
						}
					}

					// DELETE
					if (deletedIds.Count > 0) {
						string ids = string.Join(",", deletedIds);
						
						string checkFkQuery =
							"IF EXISTS (SELECT 1 FROM ISEE_PNP_AgendaUjian WHERE KelompokDistribusiID IN (" + ids + ")) " +
							"THROW 50000, 'Kelompok Distribusi tidak dapat dihapus karena sedang digunakan pada modul Distribusi Soal.', 1; " +
							"IF EXISTS (SELECT 1 FROM ISEE_PNP_ParameterScoring WHERE KelompokDistribusiID IN (" + ids + ")) " +
							"THROW 50000, 'Kelompok Distribusi tidak dapat dihapus karena sedang digunakan pada modul Scoring Parameter.', 1;";
                        new SqlCommand(checkFkQuery, conn, trans).ExecuteNonQuery();

						new SqlCommand(
							"DELETE FROM ISEE_PNP_MemberDistribusi " +
							"WHERE KelompokDistribusiID IN (" + ids + ")",
							conn,
							trans
						).ExecuteNonQuery();


						new SqlCommand(
							"DELETE FROM ISEE_PNP_DistribusiJenisSoal " +
							"WHERE KelompokDistribusiID IN (" + ids + ")",
							conn,
							trans
						).ExecuteNonQuery();

						new SqlCommand(
							"DELETE FROM ISEE_PNP_KelompokDistribusi " +
							"WHERE ID IN (" + ids + ")",
							conn,
							trans
						).ExecuteNonQuery();

						foreach (var id in deletedIds) {
							string namaLama = existingDistribusi.ContainsKey(id)
								? NormalizeText(existingDistribusi[id])
								: "Unknown";

							string actionDesc = string.Format(
								"Menghapus Kelompok Distribusi '{0}'",
								namaLama
							);

							InsertHistoryLogInTransaction(
								conn,
								trans,
								userId,
								actionDesc,
								"KelompokDistribusi",
								id.ToString()
							);
						}
					}

					// CREATE / UPDATE
					foreach (var item in data) {
						string namaBaru = NormalizeText(item.NamaKelompok);
						if (string.IsNullOrEmpty(namaBaru)) continue;

                        string dupQuery = item.ID == 0 
                            ? "SELECT COUNT(1) FROM ISEE_PNP_KelompokDistribusi WHERE NamaKelompok = @nama"
                            : "SELECT COUNT(1) FROM ISEE_PNP_KelompokDistribusi WHERE NamaKelompok = @nama AND ID <> @id";
                        
                        var cmdDup = new SqlCommand(dupQuery, conn, trans);
                        cmdDup.Parameters.AddWithValue("@nama", namaBaru);
                        if (item.ID > 0) cmdDup.Parameters.AddWithValue("@id", item.ID);
                        
                        if (Convert.ToInt32(cmdDup.ExecuteScalar()) > 0) {
                            throw new Exception("Nama Kelompok '" + namaBaru + "' sudah digunakan.");
                        }

						// CREATE
						if (item.ID == 0) {
							string queryIns = @"
								INSERT INTO ISEE_PNP_KelompokDistribusi (NamaKelompok, CreatedBy, CreatedAt)
								OUTPUT INSERTED.ID
								VALUES (@nama, @uid, GETDATE())";

							var cmdIns = new SqlCommand(queryIns, conn, trans);
							cmdIns.Parameters.AddWithValue("@nama", namaBaru);
							cmdIns.Parameters.AddWithValue("@uid", userId);

							item.ID = Convert.ToInt32(cmdIns.ExecuteScalar());

							string actionDesc = string.Format(
								"Menambahkan Kelompok Distribusi baru '{0}'",
								namaBaru
							);

							InsertHistoryLogInTransaction(
								conn,
								trans,
								userId,
								actionDesc,
								"KelompokDistribusi",
								item.ID.ToString()
							);
						}
						// UPDATE
						else {
							if (!existingDistribusi.ContainsKey(item.ID)) {
								continue;
							}

							string namaLama = NormalizeText(existingDistribusi[item.ID]);

							// Jika tidak berubah, skip
							if (namaLama == namaBaru) {
								continue;
							}

							string queryUpd = @"
								UPDATE ISEE_PNP_KelompokDistribusi
								SET NamaKelompok = @nama,
									UpdatedBy = @uid,
									UpdatedAt = GETDATE()
								WHERE ID = @id";

							var cmdUpd = new SqlCommand(queryUpd, conn, trans);
							cmdUpd.Parameters.AddWithValue("@nama", namaBaru);
							cmdUpd.Parameters.AddWithValue("@uid", userId);
							cmdUpd.Parameters.AddWithValue("@id", item.ID);
							cmdUpd.ExecuteNonQuery();

							string actionDesc = string.Format(
								"Mengubah Kelompok Distribusi dari '{0}' menjadi '{1}'",
								namaLama,
								namaBaru
							);

							InsertHistoryLogInTransaction(
								conn,
								trans,
								userId,
								actionDesc,
								"KelompokDistribusi",
								item.ID.ToString()
							);
						}
					}

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

    public void SaveMembers(int distribusiId, List<MemberDistribusiModel> data, List<int> deletedIds, string userId) {
		data = data ?? new List<MemberDistribusiModel>();
		deletedIds = deletedIds ?? new List<int>();

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

			using (var trans = conn.BeginTransaction()) {
				try {
					string kelompokNama = "";

					var cmdKelompok = new SqlCommand(
						"SELECT NamaKelompok FROM ISEE_PNP_KelompokDistribusi WHERE ID = @id",
						conn,
						trans
					);

					cmdKelompok.Parameters.AddWithValue("@id", distribusiId);

					var kelompokResult = cmdKelompok.ExecuteScalar();

					if (kelompokResult != null && kelompokResult != DBNull.Value) {
						kelompokNama = NormalizeText(kelompokResult.ToString());
					}

					var existingMembers = new Dictionary<int, MemberDistribusiModel>();

					var idsToFetch = deletedIds
						.Concat(data.Where(d => d.ID > 0).Select(d => d.ID))
						.Distinct()
						.ToList();

					if (idsToFetch.Count > 0) {
						string idList = string.Join(",", idsToFetch);

						var cmdFetch = new SqlCommand(
							"SELECT ID, KelompokDistribusiID, Skill, Position " +
							"FROM ISEE_PNP_MemberDistribusi " +
							"WHERE ID IN (" + idList + ")",
							conn,
							trans
						);

						using (var reader = cmdFetch.ExecuteReader()) {
							while (reader.Read()) {
								existingMembers[reader.GetInt32(0)] = new MemberDistribusiModel {
									ID = reader.GetInt32(0),
									KelompokDistribusiID = reader.GetInt32(1),
									Skill = reader.IsDBNull(2) ? "" : reader.GetString(2),
									Position = reader.IsDBNull(3) ? "" : reader.GetString(3)
								};
							}
						}
					}

					// DELETE
					if (deletedIds.Count > 0) {
						string ids = string.Join(",", deletedIds);

						new SqlCommand(
							"DELETE FROM ISEE_PNP_MemberDistribusi " +
							"WHERE ID IN (" + ids + ")",
							conn,
							trans
						).ExecuteNonQuery();

						foreach (var id in deletedIds) {
							MemberDistribusiModel oldMember;

							if (existingMembers.TryGetValue(id, out oldMember)) {
								string oldSkill = NormalizeText(oldMember.Skill);
								string oldPosition = NormalizeText(oldMember.Position);

								string actionDesc = string.Format(
									"Menghapus Member dengan Skill '{0}' dan Position '{1}' dari Kelompok Distribusi '{2}'",
									oldSkill,
									oldPosition,
									kelompokNama
								);

								InsertHistoryLogInTransaction(
									conn,
									trans,
									userId,
									actionDesc,
									"MemberDistribusi",
									id.ToString()
								);
							}
						}
					}

					// CREATE / UPDATE
					foreach (var item in data) {
						string skillBaru = NormalizeText(item.Skill);
						string positionBaru = NormalizeText(item.Position);
						if (string.IsNullOrEmpty(skillBaru) || string.IsNullOrEmpty(positionBaru)) continue;

                        string dupMemQuery = item.ID == 0
                            ? "SELECT COUNT(1) FROM ISEE_PNP_MemberDistribusi WHERE KelompokDistribusiID = @did AND Skill = @skill AND Position = @position"
                            : "SELECT COUNT(1) FROM ISEE_PNP_MemberDistribusi WHERE KelompokDistribusiID = @did AND Skill = @skill AND Position = @position AND ID <> @id";

                        var cmdDupMem = new SqlCommand(dupMemQuery, conn, trans);
                        cmdDupMem.Parameters.AddWithValue("@did", distribusiId);
                        cmdDupMem.Parameters.AddWithValue("@skill", skillBaru);
                        cmdDupMem.Parameters.AddWithValue("@position", positionBaru);
                        if (item.ID > 0) cmdDupMem.Parameters.AddWithValue("@id", item.ID);

                        if (Convert.ToInt32(cmdDupMem.ExecuteScalar()) > 0) {
                            throw new Exception("Kombinasi Skill '" + skillBaru + "' dan Position '" + positionBaru + "' sudah ada dalam kelompok ini.");
                        }

						// CREATE
						if (item.ID == 0) {
							string queryIns = @"
								INSERT INTO ISEE_PNP_MemberDistribusi (
									KelompokDistribusiID,
									Skill,
									Position,
									CreatedBy,
									CreatedAt
								)
								OUTPUT INSERTED.ID
								VALUES (@did, @skill, @position, @uid, GETDATE())";

							var cmdIns = new SqlCommand(queryIns, conn, trans);
							cmdIns.Parameters.AddWithValue("@did", distribusiId);
							cmdIns.Parameters.AddWithValue("@skill", skillBaru);
							cmdIns.Parameters.AddWithValue("@position", positionBaru);
							cmdIns.Parameters.AddWithValue("@uid", userId);

							item.ID = Convert.ToInt32(cmdIns.ExecuteScalar());

							string actionDesc = string.Format(
								"Menambahkan Member baru dengan Skill '{0}' dan Position '{1}' ke Kelompok Distribusi '{2}'",
								skillBaru,
								positionBaru,
								kelompokNama
							);

							InsertHistoryLogInTransaction(
								conn,
								trans,
								userId,
								actionDesc,
								"MemberDistribusi",
								item.ID.ToString()
							);
						}
						// UPDATE
						else {
							MemberDistribusiModel oldMember;

							if (!existingMembers.TryGetValue(item.ID, out oldMember)) {
								continue;
							}

							string skillLama = NormalizeText(oldMember.Skill);
							string positionLama = NormalizeText(oldMember.Position);

							// Jika tidak berubah, skip
							if (skillLama == skillBaru && positionLama == positionBaru) {
								continue;
							}

							string queryUpd = @"
								UPDATE ISEE_PNP_MemberDistribusi
								SET Skill = @skill,
									Position = @position,
									UpdatedBy = @uid,
									UpdatedAt = GETDATE()
								WHERE ID = @id";

							var cmdUpd = new SqlCommand(queryUpd, conn, trans);
							cmdUpd.Parameters.AddWithValue("@skill", skillBaru);
							cmdUpd.Parameters.AddWithValue("@position", positionBaru);
							cmdUpd.Parameters.AddWithValue("@uid", userId);
							cmdUpd.Parameters.AddWithValue("@id", item.ID);
							cmdUpd.ExecuteNonQuery();

							string actionDesc = string.Format(
								"Mengubah Member di Kelompok Distribusi '{0}' dari Skill '{1}' Position '{2}' menjadi Skill '{3}' Position '{4}'",
								kelompokNama,
								skillLama,
								positionLama,
								skillBaru,
								positionBaru
							);

							InsertHistoryLogInTransaction(
								conn,
								trans,
								userId,
								actionDesc,
								"MemberDistribusi",
								item.ID.ToString()
							);
						}
					}

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

    public void SaveDistribusiJenisSoal(int distribusiId, List<DistribusiJenisSoalModel> data, List<int> deletedIds, string userId) {
		data = data ?? new List<DistribusiJenisSoalModel>();
		deletedIds = deletedIds ?? new List<int>();

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

			using (var trans = conn.BeginTransaction()) {
				try {
					string kelompokNama = "";

					var cmdKelompok = new SqlCommand(
						"SELECT NamaKelompok FROM ISEE_PNP_KelompokDistribusi WHERE ID = @id",
						conn,
						trans
					);

					cmdKelompok.Parameters.AddWithValue("@id", distribusiId);

					var kelompokResult = cmdKelompok.ExecuteScalar();

					if (kelompokResult != null && kelompokResult != DBNull.Value) {
						kelompokNama = NormalizeText(kelompokResult.ToString());
					}

					var existingRelation = new Dictionary<int, int>();

					var relationIdsToFetch = deletedIds
						.Concat(
							data
								.Where(d => d.ID > 0)
								.Select(d => d.ID)
						)
						.Distinct()
						.ToList();

					if (relationIdsToFetch.Count > 0) {
						string idList = string.Join(",", relationIdsToFetch);

						var cmdFetch = new SqlCommand(
							"SELECT ID, JenisSoalID " +
							"FROM ISEE_PNP_DistribusiJenisSoal " +
							"WHERE KelompokDistribusiID = @did " +
							"AND ID IN (" + idList + ")",
							conn,
							trans
						);

						cmdFetch.Parameters.AddWithValue("@did", distribusiId);

						using (var reader = cmdFetch.ExecuteReader()) {
							while (reader.Read()) {
								existingRelation[reader.GetInt32(0)] = reader.GetInt32(1);
							}
						}
					}

					var jenisSoalIds = existingRelation.Values
						.Concat(
							data
								.Where(d => d.JenisSoalID > 0)
								.Select(d => d.JenisSoalID)
						)
						.Distinct()
						.ToList();

					var jenisSoalNames = new Dictionary<int, string>();

					if (jenisSoalIds.Count > 0) {
						string jsIdList = string.Join(",", jenisSoalIds);

						var cmdJS = new SqlCommand(
							"SELECT ID, NamaJenisSoal " +
							"FROM ISEE_PNP_JenisSoal " +
							"WHERE ID IN (" + jsIdList + ")",
							conn,
							trans
						);

						using (var reader = cmdJS.ExecuteReader()) {
							while (reader.Read()) {
								jenisSoalNames[reader.GetInt32(0)] = reader.IsDBNull(1)
									? ""
									: reader.GetString(1);
							}
						}
					}

					// ================================
					// DELETE
					// ================================
					if (deletedIds.Count > 0) {
						string ids = string.Join(",", deletedIds);

						new SqlCommand(
							"DELETE FROM ISEE_PNP_DistribusiJenisSoal " +
							"WHERE ID IN (" + ids + ")",
							conn,
							trans
						).ExecuteNonQuery();

						foreach (var id in deletedIds) {
							int jenisSoalId;

							if (existingRelation.TryGetValue(id, out jenisSoalId)) {
								string jenisSoalNama = jenisSoalNames.ContainsKey(jenisSoalId)
									? jenisSoalNames[jenisSoalId]
									: "Unknown";

								string actionDesc = string.Format(
									"Menghapus Jenis Soal '{0}' dari Kelompok Distribusi '{1}'",
									jenisSoalNama,
									kelompokNama
								);

								InsertHistoryLogInTransaction(
									conn,
									trans,
									userId,
									actionDesc,
									"JenisSoalDistribusi",
									id.ToString()
								);
							}
						}
					}

					// ================================
					// CREATE / UPDATE
					// ================================
					foreach (var item in data) {
						if (item.JenisSoalID <= 0) {
							continue;
						}

						// ================================
						// CREATE
						// ================================
						if (item.ID == 0) {
							var cmdCheck = new SqlCommand(@"
								SELECT COUNT(*)
								FROM ISEE_PNP_DistribusiJenisSoal
								WHERE KelompokDistribusiID = @did
								  AND JenisSoalID = @jid",
								conn,
								trans
							);

							cmdCheck.Parameters.AddWithValue("@did", distribusiId);
							cmdCheck.Parameters.AddWithValue("@jid", item.JenisSoalID);

							int count = Convert.ToInt32(cmdCheck.ExecuteScalar());

							if (count > 0) {
								continue;
							}

							string queryIns = @"
								INSERT INTO ISEE_PNP_DistribusiJenisSoal (
									KelompokDistribusiID,
									JenisSoalID,
									CreatedBy,
									CreatedAt
								)
								OUTPUT INSERTED.ID
								VALUES (
									@did,
									@jid,
									@uid,
									GETDATE()
								)";

							var cmdIns = new SqlCommand(queryIns, conn, trans);

							cmdIns.Parameters.AddWithValue("@did", distribusiId);
							cmdIns.Parameters.AddWithValue("@jid", item.JenisSoalID);
							cmdIns.Parameters.AddWithValue("@uid", userId);

							item.ID = Convert.ToInt32(cmdIns.ExecuteScalar());

							string jenisSoalNama = jenisSoalNames.ContainsKey(item.JenisSoalID)
								? jenisSoalNames[item.JenisSoalID]
								: "Unknown";

							string actionDesc = string.Format(
								"Menambahkan Jenis Soal '{0}' ke Kelompok Distribusi '{1}'",
								jenisSoalNama,
								kelompokNama
							);

							InsertHistoryLogInTransaction(
								conn,
								trans,
								userId,
								actionDesc,
								"JenisSoalDistribusi",
								item.ID.ToString()
							);
						}

						// ================================
						// UPDATE
						// ================================
						else {
							int jenisSoalLamaId;

							if (!existingRelation.TryGetValue(item.ID, out jenisSoalLamaId)) {
								continue;
							}

							if (jenisSoalLamaId == item.JenisSoalID) {
								continue;
							}

							var cmdCheck = new SqlCommand(@"
								SELECT COUNT(*)
								FROM ISEE_PNP_DistribusiJenisSoal
								WHERE KelompokDistribusiID = @did
								  AND JenisSoalID = @jid
								  AND ID <> @id",
								conn,
								trans
							);

							cmdCheck.Parameters.AddWithValue("@did", distribusiId);
							cmdCheck.Parameters.AddWithValue("@jid", item.JenisSoalID);
							cmdCheck.Parameters.AddWithValue("@id", item.ID);

							int count = Convert.ToInt32(cmdCheck.ExecuteScalar());

							if (count > 0) {
								continue;
							}

							string queryUpd = @"
								UPDATE ISEE_PNP_DistribusiJenisSoal
								SET JenisSoalID = @jid,
									UpdatedBy = @uid,
									UpdatedAt = GETDATE()
								WHERE ID = @id
								  AND KelompokDistribusiID = @did";

							var cmdUpd = new SqlCommand(queryUpd, conn, trans);

							cmdUpd.Parameters.AddWithValue("@jid", item.JenisSoalID);
							cmdUpd.Parameters.AddWithValue("@uid", userId);
							cmdUpd.Parameters.AddWithValue("@id", item.ID);
							cmdUpd.Parameters.AddWithValue("@did", distribusiId);

							int affectedRows = cmdUpd.ExecuteNonQuery();

							if (affectedRows <= 0) {
								continue;
							}

							string jenisSoalLamaNama =
								jenisSoalNames.ContainsKey(jenisSoalLamaId)
									? jenisSoalNames[jenisSoalLamaId]
									: "Unknown";

							string jenisSoalBaruNama =
								jenisSoalNames.ContainsKey(item.JenisSoalID)
									? jenisSoalNames[item.JenisSoalID]
									: "Unknown";

							string actionDesc = string.Format(
								"Mengubah Jenis Soal pada Kelompok Distribusi '{0}' dari '{1}' menjadi '{2}'",
								kelompokNama,
								jenisSoalLamaNama,
								jenisSoalBaruNama
							);

							InsertHistoryLogInTransaction(
								conn,
								trans,
								userId,
								actionDesc,
								"JenisSoalDistribusi",
								item.ID.ToString()
							);
						}
					}

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

  1. ParameterScoringRepository.cs

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Configuration;
using System.Linq;

public class ParameterScoringRepository
{
    private string connString = ConfigurationManager.ConnectionStrings["iSee"].ConnectionString;

    private string NormalizeText(string value)
    {
        return (value ?? string.Empty).Trim();
    }

    // ==================== GET DATA ====================
    public List<ParameterScoringModel> GetScoring(List<int> kelompokIds, List<string> skills, string search = "")
    {
        var list = new List<ParameterScoringModel>();

        using (var conn = new SqlConnection(connString))
        {
            string query = @"
                SELECT a.ID, a.KelompokDistribusiID, b.NamaKelompok, a.Skill, a.Level, a.NilaiTarget
                FROM ISEE_PNP_ParameterScoring a
                JOIN ISEE_PNP_KelompokDistribusi b ON a.KelompokDistribusiID = b.ID
                WHERE 1 = 1";

            var cmd = new SqlCommand();

            if (kelompokIds != null && kelompokIds.Count > 0)
            {
                var idParams = new List<string>();

                for (int i = 0; i < kelompokIds.Count; i++)
                {
                    string pName = "@kid" + i;
                    idParams.Add(pName);
                    cmd.Parameters.AddWithValue(pName, kelompokIds[i]);
                }

                query += " AND a.KelompokDistribusiID IN (" + string.Join(",", idParams) + ")";
            }

            if (skills != null && skills.Count > 0)
            {
                var skillParams = new List<string>();

                for (int i = 0; i < skills.Count; i++)
                {
                    string pName = "@skill" + i;
                    skillParams.Add(pName);
                    cmd.Parameters.AddWithValue(pName, skills[i]);
                }

                query += " AND a.Skill IN (" + string.Join(",", skillParams) + ")";
            }

            if (!string.IsNullOrWhiteSpace(search))
            {
                query += @" AND (b.NamaKelompok LIKE @search
                              OR a.Skill LIKE @search
                              OR a.Level LIKE @search
                              OR CONVERT(nvarchar(50), a.NilaiTarget) LIKE @search)";

                cmd.Parameters.AddWithValue("@search", "%" + search + "%");
            }

            query += " ORDER BY b.NamaKelompok, a.Skill, a.Level";

            cmd.CommandText = query;
            cmd.Connection = conn;

            conn.Open();

            using (var reader = cmd.ExecuteReader())
            {
                while (reader.Read())
                {
                    list.Add(new ParameterScoringModel
                    {
                        ID = reader.GetInt32(0),
                        KelompokDistribusiID = reader.GetInt32(1),
                        NamaKelompok = reader.IsDBNull(2) ? "" : reader.GetString(2),
                        Skill = reader.IsDBNull(3) ? "" : reader.GetString(3),
                        Level = reader.IsDBNull(4) ? "" : reader.GetString(4),
                        NilaiTarget = reader.GetDecimal(5)
                    });
                }
            }
        }

        return list;
    }

    // ==================== PARENT-CHILD: SKILL BERDASARKAN KELOMPOK DISTRIBUSI ====================
    public List<string> GetSkillsByKelompok(int kelompokId)
    {
        var list = new List<string>();

        if (kelompokId <= 0) return list;

        using (var conn = new SqlConnection(connString))
        {
            string query = @"
                SELECT DISTINCT m.Skill
                FROM ISEE_PNP_MemberDistribusi m
                WHERE m.KelompokDistribusiID = @kid
                  AND m.Skill IS NOT NULL
                  AND LTRIM(RTRIM(m.Skill)) <> ''
                ORDER BY m.Skill ASC";

            var cmd = new SqlCommand(query, conn);
            cmd.Parameters.AddWithValue("@kid", kelompokId);

            conn.Open();

            using (var reader = cmd.ExecuteReader())
            {
                while (reader.Read())
                {
                    list.Add(reader.GetString(0));
                }
            }
        }

        return list;
    }

    // ==================== LEVEL ====================
    public List<ParameterLevel> GetAllLevel()
    {
        var list = new List<ParameterLevel>();

        using (var conn = new SqlConnection(connString))
        {
            var cmd = new SqlCommand(@"
                SELECT DISTINCT SDM_LEVEL
                FROM ISEE_STAFF_DATA
                WHERE SDM_LEVEL IS NOT NULL
                ORDER BY SDM_LEVEL ASC", conn);

            conn.Open();

            using (var reader = cmd.ExecuteReader())
            {
                while (reader.Read())
                {
                    list.Add(new ParameterLevel
                    {
                        SDM_LEVEL = reader.GetString(0)
                    });
                }
            }
        }

        return list;
    }

    // ==================== HISTORY LOG ====================
    public List<HistoryLogModel> GetLogScoring(DateTime? dateFrom, DateTime? dateTo)
    {
        var list = new List<HistoryLogModel>();

        using (var conn = new SqlConnection(connString))
        {
            string query = @"
                SELECT l.ID,
                       l.CreatedAt,
                       l.DomainID,
                       ISNULL(u.Full_Name, l.DomainID) AS UserName,
                       l.Action,
                       l.EntityType,
                       l.EntityID
                FROM ISEE_PNP_HistoryLog l
                LEFT JOIN ISEE_STAFF_DATA u ON l.DomainID = u.Domain_ID
                WHERE l.EntityType = 'ParameterScoring'";

            if (dateFrom.HasValue)
            {
                query += " AND l.CreatedAt >= @dateFrom";
            }

            if (dateTo.HasValue)
            {
                query += " AND l.CreatedAt <= @dateTo";
            }

            query += " ORDER BY l.CreatedAt DESC";

            var cmd = new SqlCommand(query, conn);

            if (dateFrom.HasValue)
            {
                cmd.Parameters.AddWithValue("@dateFrom", dateFrom.Value);
            }

            if (dateTo.HasValue)
            {
                cmd.Parameters.AddWithValue("@dateTo", dateTo.Value.AddDays(1).AddSeconds(-1));
            }

            conn.Open();

            using (var reader = cmd.ExecuteReader())
            {
                while (reader.Read())
                {
                    list.Add(new HistoryLogModel
                    {
                        ID = reader.GetInt32(0),
                        CreatedAt = reader.GetDateTime(1),
                        DomainID = reader.IsDBNull(2) ? "" : reader.GetString(2),
                        UserName = reader.IsDBNull(3) ? "" : reader.GetString(3),
                        Action = reader.IsDBNull(4) ? "" : reader.GetString(4),
                        EntityType = reader.IsDBNull(5) ? "" : reader.GetString(5),
                        EntityID = reader.IsDBNull(6) ? "" : reader.GetString(6)
                    });
                }
            }
        }

        return list;
    }

    private void InsertHistoryLogInTransaction(
        SqlConnection conn,
        SqlTransaction trans,
        string domainID,
        string action,
        string entityType,
        string entityID)
    {
        string query = @"
            INSERT INTO ISEE_PNP_HistoryLog (
                DomainID,
                Action,
                EntityType,
                EntityID,
                CreatedAt
            )
            VALUES (@domainID, @action, @entityType, @entityID, GETDATE())";

        var cmd = new SqlCommand(query, conn, trans);

        cmd.Parameters.AddWithValue("@domainID", domainID);
        cmd.Parameters.AddWithValue("@action", action);
        cmd.Parameters.AddWithValue("@entityType", entityType);
        cmd.Parameters.AddWithValue("@entityID", entityID);

        cmd.ExecuteNonQuery();
    }

    // ==================== BULK UPSERT + HARD DELETE + LOGGING ====================
    public void SaveScoring(List<ParameterScoringModel> data, List<int> deletedIds, string userId)
    {
        data = data ?? new List<ParameterScoringModel>();
        deletedIds = deletedIds ?? new List<int>();

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

            using (var trans = conn.BeginTransaction())
            {
                try
                {
                    // ==================================================
                    // 1. Ambil data existing untuk kebutuhan log & compare
                    // ==================================================
                    var existingScoring = new Dictionary<int, ParameterScoringModel>();

                    var idsToFetch = deletedIds
                        .Concat(data.Where(d => d.ID > 0).Select(d => d.ID))
                        .Distinct()
                        .ToList();

                    if (idsToFetch.Count > 0)
                    {
                        string idList = string.Join(",", idsToFetch);

                        var cmdFetch = new SqlCommand(@"
                            SELECT a.ID,
                                   a.KelompokDistribusiID,
                                   b.NamaKelompok,
                                   a.Skill,
                                   a.Level,
                                   a.NilaiTarget
                            FROM ISEE_PNP_ParameterScoring a
                            JOIN ISEE_PNP_KelompokDistribusi b ON a.KelompokDistribusiID = b.ID
                            WHERE a.ID IN (" + idList + ")", conn, trans);

                        using (var reader = cmdFetch.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                existingScoring[reader.GetInt32(0)] = new ParameterScoringModel
                                {
                                    ID = reader.GetInt32(0),
                                    KelompokDistribusiID = reader.GetInt32(1),
                                    NamaKelompok = reader.IsDBNull(2) ? "" : reader.GetString(2),
                                    Skill = reader.IsDBNull(3) ? "" : reader.GetString(3),
                                    Level = reader.IsDBNull(4) ? "" : reader.GetString(4),
                                    NilaiTarget = reader.GetDecimal(5)
                                };
                            }
                        }
                    }

                    // ==================================================
                    // 2. Ambil nama kelompok distribusi untuk log
                    // ==================================================
                    var kelompokIds = data
                        .Where(d => d.KelompokDistribusiID > 0)
                        .Select(d => d.KelompokDistribusiID)
                        .Concat(existingScoring.Values.Select(x => x.KelompokDistribusiID))
                        .Distinct()
                        .ToList();

                    var kelompokNames = new Dictionary<int, string>();

                    if (kelompokIds.Count > 0)
                    {
                        string kelompokIdList = string.Join(",", kelompokIds);

                        var cmdKelompok = new SqlCommand(@"
                            SELECT ID, NamaKelompok
                            FROM ISEE_PNP_KelompokDistribusi
                            WHERE ID IN (" + kelompokIdList + ")", conn, trans);

                        using (var reader = cmdKelompok.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                kelompokNames[reader.GetInt32(0)] = reader.IsDBNull(1) ? "" : reader.GetString(1);
                            }
                        }
                    }

                    // ==================================================
                    // 3. DELETE
                    // ==================================================
                    if (deletedIds.Count > 0)
                    {
                        string ids = string.Join(",", deletedIds);

                        new SqlCommand(
                            string.Format("DELETE FROM ISEE_PNP_ParameterScoring WHERE ID IN ({0})", ids),
                            conn,
                            trans
                        ).ExecuteNonQuery();

                        foreach (var id in deletedIds)
                        {
                            ParameterScoringModel old;

                            if (existingScoring.TryGetValue(id, out old))
                            {
                                string namaKelompokLama = NormalizeText(old.NamaKelompok);

                                if (string.IsNullOrEmpty(namaKelompokLama))
                                {
                                    string fallbackNama;
                                    if (kelompokNames.TryGetValue(old.KelompokDistribusiID, out fallbackNama))
                                    {
                                        namaKelompokLama = fallbackNama;
                                    }
                                    else
                                    {
                                        namaKelompokLama = "Unknown";
                                    }
                                }

                                string actionDesc = string.Format(
                                    "Menghapus Parameter Scoring pada Kelompok '{0}', Skill '{1}', Level '{2}' dengan Nilai Target '{3}'",
                                    namaKelompokLama,
                                    NormalizeText(old.Skill),
                                    NormalizeText(old.Level),
                                    old.NilaiTarget
                                );

                                InsertHistoryLogInTransaction(
                                    conn,
                                    trans,
                                    userId,
                                    actionDesc,
                                    "ParameterScoring",
                                    id.ToString()
                                );
                            }
                        }
                    }

                    // ==================================================
                    // 4. CREATE / UPDATE
                    // ==================================================
                    foreach (var item in data)
                    {
                        string skillBaru = NormalizeText(item.Skill);
                        string levelBaru = NormalizeText(item.Level);

                        if (item.KelompokDistribusiID <= 0 || string.IsNullOrEmpty(skillBaru) || string.IsNullOrEmpty(levelBaru))
                        {
                            continue;
                        }

                        string namaKelompokBaru;
                        if (!kelompokNames.TryGetValue(item.KelompokDistribusiID, out namaKelompokBaru))
                        {
                            namaKelompokBaru = "Unknown";
                        }

                        // ==================== CREATE ====================
                        if (item.ID == 0)
                        {
                            string queryIns = @"
                                INSERT INTO ISEE_PNP_ParameterScoring
                                (
                                    KelompokDistribusiID,
                                    Skill,
                                    Level,
                                    NilaiTarget,
                                    CreatedBy,
                                    CreatedAt
                                )
                                OUTPUT INSERTED.ID
                                VALUES
                                (
                                    @did,
                                    @skill,
                                    @lvl,
                                    @nilai,
                                    @uid,
                                    GETDATE()
                                )";

                            var cmdIns = new SqlCommand(queryIns, conn, trans);

                            cmdIns.Parameters.AddWithValue("@did", item.KelompokDistribusiID);
                            cmdIns.Parameters.AddWithValue("@skill", skillBaru);
                            cmdIns.Parameters.AddWithValue("@lvl", levelBaru);
                            cmdIns.Parameters.AddWithValue("@nilai", item.NilaiTarget);
                            cmdIns.Parameters.AddWithValue("@uid", userId);

                            item.ID = Convert.ToInt32(cmdIns.ExecuteScalar());

                            string actionDesc = string.Format(
                                "Menambahkan Parameter Scoring baru pada Kelompok '{0}', Skill '{1}', Level '{2}' dengan Nilai Target '{3}'",
                                namaKelompokBaru,
                                skillBaru,
                                levelBaru,
                                item.NilaiTarget
                            );

                            InsertHistoryLogInTransaction(
                                conn,
                                trans,
                                userId,
                                actionDesc,
                                "ParameterScoring",
                                item.ID.ToString()
                            );
                        }
                        // ==================== UPDATE ====================
                        else
                        {
                            ParameterScoringModel old;

                            if (!existingScoring.TryGetValue(item.ID, out old))
                            {
                                continue;
                            }

                            string namaKelompokLama = NormalizeText(old.NamaKelompok);

                            if (string.IsNullOrEmpty(namaKelompokLama))
                            {
                                string fallbackNama;
                                if (kelompokNames.TryGetValue(old.KelompokDistribusiID, out fallbackNama))
                                {
                                    namaKelompokLama = fallbackNama;
                                }
                                else
                                {
                                    namaKelompokLama = "Unknown";
                                }
                            }

                            string skillLama = NormalizeText(old.Skill);
                            string levelLama = NormalizeText(old.Level);

                            // Jika tidak ada perubahan, skip
                            if (
                                old.KelompokDistribusiID == item.KelompokDistribusiID &&
                                skillLama == skillBaru &&
                                levelLama == levelBaru &&
                                old.NilaiTarget == item.NilaiTarget
                            )
                            {
                                continue;
                            }

                            string queryUpd = @"
                                UPDATE ISEE_PNP_ParameterScoring
                                SET KelompokDistribusiID = @did,
                                    Skill = @skill,
                                    Level = @lvl,
                                    NilaiTarget = @nilai,
                                    UpdatedBy = @uid,
                                    UpdatedAt = GETDATE()
                                WHERE ID = @id";

                            var cmdUpd = new SqlCommand(queryUpd, conn, trans);

                            cmdUpd.Parameters.AddWithValue("@did", item.KelompokDistribusiID);
                            cmdUpd.Parameters.AddWithValue("@skill", skillBaru);
                            cmdUpd.Parameters.AddWithValue("@lvl", levelBaru);
                            cmdUpd.Parameters.AddWithValue("@nilai", item.NilaiTarget);
                            cmdUpd.Parameters.AddWithValue("@uid", userId);
                            cmdUpd.Parameters.AddWithValue("@id", item.ID);

                            cmdUpd.ExecuteNonQuery();

                            string actionDesc = string.Format(
                                "Mengubah Parameter Scoring dari Kelompok '{0}', Skill '{1}', Level '{2}', Nilai Target '{3}' menjadi Kelompok '{4}', Skill '{5}', Level '{6}', Nilai Target '{7}'",
                                namaKelompokLama,
                                skillLama,
                                levelLama,
                                old.NilaiTarget,
                                namaKelompokBaru,
                                skillBaru,
                                levelBaru,
                                item.NilaiTarget
                            );

                            InsertHistoryLogInTransaction(
                                conn,
                                trans,
                                userId,
                                actionDesc,
                                "ParameterScoring",
                                item.ID.ToString()
                            );
                        }
                    }

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

Atau bisa dilihat yang ada pada Dashboard yang telah berhasil menghitung dengan sesuai:

using Models.pnp.dashboard;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Linq;

namespace Repositories.pnp.dashboard
{
    public class DashboardRepository
    {
        private readonly string _connStr =
            ConfigurationManager.ConnectionStrings["iSee"].ConnectionString;

        private const string FamCte = @"
			;WITH Fam AS (
				SELECT a.ID AS AgendaID, a.ID AS RootID
				FROM ISEE_PNP_AgendaUjian a
				WHERE a.ParentAgendaID IS NULL
				UNION ALL
				SELECT c.ID, f.RootID
				FROM ISEE_PNP_AgendaUjian c
				INNER JOIN Fam f ON c.ParentAgendaID = f.AgendaID
			)";

        private const string RecursionHint = " OPTION (MAXRECURSION 50)";

        private static string PeriodeExpr(string alias)
        {
            return "ISNULL(NULLIF(LTRIM(RTRIM(" + alias + ".PeriodeBerjalan)), ''), CONVERT(char(7), " + alias + ".TanggalMulai, 120))";
        }

        private const string DurasiApply = @"
			CROSS APPLY (
				SELECT
					CASE WHEN up.MulaiUjian IS NULL OR up.SelesaiUjian IS NULL OR up.SelesaiUjian < up.MulaiUjian
						 THEN NULL
						 ELSE DATEDIFF(SECOND, up.MulaiUjian, up.SelesaiUjian) END AS Wall,
					CASE WHEN up.SisaWaktuDetik IS NOT NULL AND a.DurasiMenit > 0
						 THEN a.DurasiMenit * 60 - up.SisaWaktuDetik
						 ELSE NULL END AS ByRemaining
			) tw
			CROSS APPLY (
				SELECT CASE
					WHEN tw.Wall IS NULL THEN NULL
					WHEN tw.ByRemaining IS NOT NULL AND tw.ByRemaining > 0 AND tw.ByRemaining <= tw.Wall THEN tw.ByRemaining
					WHEN a.DurasiMenit > 0 AND tw.Wall > a.DurasiMenit * 60 THEN a.DurasiMenit * 60
					ELSE tw.Wall END AS DurasiDetik
			) td";

        private const string PesertaCtes = @",
			Peserta AS (
				SELECT r.ID AS RootID, UPPER(LTRIM(RTRIM(s.Domain_ID))) AS DomainID
				FROM Roots r
				INNER JOIN ISEE_PNP_MemberDistribusi md ON md.KelompokDistribusiID = r.KelompokDistribusiID
				INNER JOIN ISEE_STAFF_DATA s
					ON LTRIM(RTRIM(s.Position)) = LTRIM(RTRIM(md.Position))
				   AND LTRIM(RTRIM(s.Job_Title)) = LTRIM(RTRIM(md.Skill))
				WHERE NULLIF(LTRIM(RTRIM(s.Domain_ID)), '') IS NOT NULL
				UNION
				SELECT f.RootID, UPPER(LTRIM(RTRIM(i.DomainID)))
				FROM ISEE_PNP_InvitePeserta i
				INNER JOIN Fam f ON f.AgendaID = i.AgendaUjianID
				INNER JOIN Roots r ON r.ID = f.RootID
				UNION
				SELECT f.RootID, UPPER(LTRIM(RTRIM(u.DomainID)))
				FROM ISEE_PNP_UjianPeserta u
				INNER JOIN Fam f ON f.AgendaID = u.AgendaUjianID
				INNER JOIN Roots r ON r.ID = f.RootID
			),
			TotalPeserta AS (
				SELECT RootID, COUNT(*) AS Total FROM Peserta GROUP BY RootID
			)";

        // Peserta yang sudah selesai (DomainID unik, termasuk agenda susulan). Butuh CTE "Roots".
        private const string SelesaiCte = @",
			Selesai AS (
				SELECT f.RootID, COUNT(DISTINCT UPPER(LTRIM(RTRIM(u.DomainID)))) AS Total
				FROM ISEE_PNP_UjianPeserta u
				INNER JOIN Fam f ON f.AgendaID = u.AgendaUjianID
				INNER JOIN Roots r ON r.ID = f.RootID
				WHERE u.SelesaiUjian IS NOT NULL
				GROUP BY f.RootID
			)";

        private const string JawabanAggCtes = @",
			Jawab AS (
				SELECT jp.BankSoalID,
					   CASE WHEN c.OpsiID IS NULL THEN 0 ELSE 1 END AS IsBenar
				FROM ISEE_PNP_JawabanPeserta jp
				INNER JOIN ISEE_PNP_UjianPeserta up ON up.ID = jp.UjianPesertaID
				INNER JOIN Fam f ON f.AgendaID = up.AgendaUjianID
				OUTER APPLY (
					SELECT TOP 1 o.ID AS OpsiID
					FROM ISEE_PNP_SoalPesertaOpsi o
					WHERE o.UjianPesertaID = jp.UjianPesertaID
					  AND o.BankSoalID = jp.BankSoalID
					  AND o.IsCorrect = 1
					  AND o.AssignedLabel = jp.JawabanDipilih
				) c
				WHERE f.RootID = @agendaId
				  AND jp.JawabanDipilih IS NOT NULL
			),
			JawabAgg AS (
				SELECT BankSoalID,
					   SUM(IsBenar) AS Benar,
					   SUM(1 - IsBenar) AS Salah
				FROM Jawab
				GROUP BY BankSoalID
			),
			Dist AS (
				SELECT ds.BankSoalID, COUNT(*) AS Cnt
				FROM ISEE_PNP_DistribusiSoal ds
				INNER JOIN Fam f ON f.AgendaID = ds.AgendaUjianID
				WHERE f.RootID = @agendaId
				GROUP BY ds.BankSoalID
			)";

        // ======================================================================
        // SHARED DROPDOWN OPTIONS
        // ======================================================================

        public List<OptionModel> GetAgendaOptions()
        {
            var list = new List<OptionModel>();

            using (var conn = new SqlConnection(_connStr))
            {
                string q = @"
					SELECT ID, NamaAgenda, FORMAT(TanggalMulai, 'yyyy-MM') AS Periode
					FROM ISEE_PNP_AgendaUjian
					WHERE ParentAgendaID IS NULL
					ORDER BY TanggalMulai DESC";

                using (var cmd = new SqlCommand(q, conn))
                {
                    conn.Open();
                    using (var reader = cmd.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            list.Add(new OptionModel
                            {
                                ID = reader.GetInt32(0),
                                NamaAgenda = reader.IsDBNull(1) ? "" : reader.GetString(1),
                                Periode = reader.IsDBNull(2) ? "" : reader.GetString(2)
                            });
                        }
                    }
                }
            }

            return list;
        }

        public List<SimpleOptionModel> GetKelompokList()
        {
            var list = new List<SimpleOptionModel>();

            using (var conn = new SqlConnection(_connStr))
            {
                var cmd = new SqlCommand("SELECT ID, NamaKelompok FROM ISEE_PNP_KelompokDistribusi ORDER BY NamaKelompok", conn);
                conn.Open();
                using (var reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                        list.Add(new SimpleOptionModel { ID = reader.GetInt32(0), NamaKelompok = reader.IsDBNull(1) ? "" : reader.GetString(1) });
                }
            }

            return list;
        }

        public List<JenisSoalOptionModel> GetJenisSoalList()
        {
            var list = new List<JenisSoalOptionModel>();

            using (var conn = new SqlConnection(_connStr))
            {
                var cmd = new SqlCommand("SELECT ID, NamaJenisSoal FROM ISEE_PNP_JenisSoal ORDER BY NamaJenisSoal", conn);
                conn.Open();
                using (var reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                        list.Add(new JenisSoalOptionModel { ID = reader.GetInt32(0), NamaJenisSoal = reader.IsDBNull(1) ? "" : reader.GetString(1) });
                }
            }

            return list;
        }

        // ======================================================================
        // 1. PASS GRADE (agenda induk + agenda susulan digabung)
        // ======================================================================

        public List<PassGradeModel> GetPassGrade(string periode, string nama, string status)
        {
            var list = new List<PassGradeModel>();
            var pars = new List<SqlParameter>();
            string filters = "";

            if (!string.IsNullOrWhiteSpace(periode))
            {
                filters += " AND Periode = @periode";
                pars.Add(new SqlParameter("@periode", SqlDbType.VarChar, 7) { Value = periode.Trim() });
            }

            if (!string.IsNullOrWhiteSpace(nama))
            {
                filters += " AND NamaAgenda LIKE @nama";
                pars.Add(new SqlParameter("@nama", SqlDbType.NVarChar, 255) { Value = "%" + nama.Trim() + "%" });
            }

            if (!string.IsNullOrWhiteSpace(status))
            {
                filters += " AND Status = @status";
                pars.Add(new SqlParameter("@status", SqlDbType.VarChar, 50) { Value = status.Trim() });
            }

            string q = FamCte + @",
				RootsRaw AS (
					SELECT a.ID, a.NamaAgenda, a.KelompokDistribusiID, a.TanggalMulai,
						   " + PeriodeExpr("a") + @" AS Periode,
						   CASE WHEN EXISTS (
									SELECT 1
									FROM Fam x
									INNER JOIN ISEE_PNP_AgendaUjian xa ON xa.ID = x.AgendaID
									WHERE x.RootID = a.ID AND xa.Status = 'Aktif')
								THEN 'Aktif' ELSE a.Status END AS Status,
						   (SELECT COUNT(*) FROM Fam x WHERE x.RootID = a.ID AND x.AgendaID <> a.ID) AS JumlahSusulan
					FROM ISEE_PNP_AgendaUjian a
					WHERE a.ParentAgendaID IS NULL
					  AND EXISTS (   -- sama seperti sebelumnya: hanya agenda yang punya parameter scoring
							SELECT 1
							FROM ISEE_PNP_ParameterScoring ps
							INNER JOIN ISEE_PNP_MemberDistribusi md
								ON md.KelompokDistribusiID = ps.KelompokDistribusiID
							   AND LTRIM(RTRIM(md.Skill)) = LTRIM(RTRIM(ps.Skill))
							WHERE ps.KelompokDistribusiID = a.KelompokDistribusiID)
				),
				Roots AS (
					SELECT * FROM RootsRaw WHERE 1 = 1" + filters + @"
				)" + PesertaCtes + SelesaiCte + @"
				SELECT r.ID, r.NamaAgenda, r.Periode,
					   ISNULL(tp.Total, 0) AS TotalPeserta,
					   ISNULL(sl.Total, 0) AS Selesai,
					   r.Status, r.JumlahSusulan
				FROM Roots r
				LEFT JOIN TotalPeserta tp ON tp.RootID = r.ID
				LEFT JOIN Selesai sl ON sl.RootID = r.ID
				ORDER BY r.TanggalMulai DESC, r.ID DESC" + RecursionHint;

            using (var conn = new SqlConnection(_connStr))
            using (var cmd = new SqlCommand(q, conn))
            {
                cmd.Parameters.AddRange(pars.ToArray());
                conn.Open();

                using (var reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        int total = reader.GetInt32(3);
                        int selesai = reader.GetInt32(4);

                        list.Add(new PassGradeModel
                        {
                            ID = reader.GetInt32(0),
                            NamaAgenda = reader.IsDBNull(1) ? "" : reader.GetString(1),
                            Periode = reader.IsDBNull(2) ? "" : reader.GetString(2),
                            TotalPeserta = total,
                            Selesai = selesai,
                            Belum = Math.Max(total - selesai, 0),
                            Status = reader.IsDBNull(5) ? "" : reader.GetString(5),
                            JumlahSusulan = reader.GetInt32(6)
                        });
                    }
                }
            }

            return list;
        }

        // ======================================================================
        // 2. LEADERBOARD (TOP 10 PER AGENDA, SUSULAN IKUT)
        // ======================================================================

        public List<LeaderboardRowModel> GetLeaderboard(int agendaId, string periode)
        {
            var list = new List<LeaderboardRowModel>();
            var pars = new List<SqlParameter> { new SqlParameter("@agendaId", agendaId) };
            string periodeFilter = "";

            if (!string.IsNullOrEmpty(periode) && periode.Length == 7)
            {
                periodeFilter = @"
      AND EXISTS (SELECT 1 FROM ISEE_PNP_AgendaUjian ra
                  WHERE ra.ID = @agendaId AND " + PeriodeExpr("ra") + " = @periode)";
                pars.Add(new SqlParameter("@periode", SqlDbType.VarChar, 7) { Value = periode });
            }

            string q = FamCte + @",
				Ranked AS (
					SELECT up.DomainID, up.Nilai, up.MulaiUjian, up.SelesaiUjian, td.DurasiDetik,
						   ROW_NUMBER() OVER (
							   PARTITION BY UPPER(up.DomainID)
							   ORDER BY up.Nilai DESC, ISNULL(td.DurasiDetik, 2147483647) ASC, up.SelesaiUjian ASC) AS rn
					FROM ISEE_PNP_UjianPeserta up
					INNER JOIN Fam f ON f.AgendaID = up.AgendaUjianID
					INNER JOIN ISEE_PNP_AgendaUjian a ON a.ID = up.AgendaUjianID
					" + DurasiApply + @"
					WHERE f.RootID = @agendaId
					  AND up.Nilai IS NOT NULL" + periodeFilter + @"
				),
				Best AS (
					SELECT rk.*,
						   COUNT(*) OVER () AS TotalDinilai,
						   AVG(CAST(rk.Nilai AS decimal(18, 4))) OVER () AS RataRata
					FROM Ranked rk
					WHERE rk.rn = 1
				)
				SELECT TOP 10
					   ISNULL(staff.Full_Name, b.DomainID) AS NamaPeserta,
					   b.DomainID, staff.Site, b.Nilai, b.MulaiUjian, b.SelesaiUjian, b.DurasiDetik,
					   b.TotalDinilai, b.RataRata
				FROM Best b
				LEFT JOIN v_ISEE_STAFF_DATA staff ON staff.Domain_ID = b.DomainID
				ORDER BY b.Nilai DESC, ISNULL(b.DurasiDetik, 2147483647) ASC, b.SelesaiUjian ASC" + RecursionHint;

            using (var conn = new SqlConnection(_connStr))
            using (var cmd = new SqlCommand(q, conn))
            {
                cmd.Parameters.AddRange(pars.ToArray());
                conn.Open();

                using (var reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        list.Add(new LeaderboardRowModel
                        {
                            NamaPeserta = reader.IsDBNull(0) ? "-" : reader.GetString(0),
                            DomainID = reader.IsDBNull(1) ? "" : reader.GetString(1),
                            Site = reader.IsDBNull(2) ? "" : reader.GetString(2),
                            Nilai = reader.IsDBNull(3) ? (decimal?)null : reader.GetDecimal(3),
                            MulaiUjian = reader.IsDBNull(4) ? (DateTime?)null : reader.GetDateTime(4),
                            SelesaiUjian = reader.IsDBNull(5) ? (DateTime?)null : reader.GetDateTime(5),
                            DurasiDetik = reader.IsDBNull(6) ? (int?)null : Convert.ToInt32(reader.GetValue(6)),
                            TotalDinilai = reader.IsDBNull(7) ? 0 : Convert.ToInt32(reader.GetValue(7)),
                            RataRata = reader.IsDBNull(8) ? (decimal?)null : Convert.ToDecimal(reader.GetValue(8))
                        });
                    }
                }
            }

            return list;
        }

        // ======================================================================
        // 3. PENGERJAAN AGENDA AKTIF (aktif bila induk ATAU susulan masih aktif)
        // ======================================================================

        public List<PengerjaanAktifModel> GetPengerjaanAktif(int? kelompokId, string nama)
        {
            var list = new List<PengerjaanAktifModel>();
            var pars = new List<SqlParameter>();
            string filters = "";

            if (kelompokId.HasValue && kelompokId.Value > 0)
            {
                filters += " AND a.KelompokDistribusiID = @kelompokId";
                pars.Add(new SqlParameter("@kelompokId", kelompokId.Value));
            }

            if (!string.IsNullOrWhiteSpace(nama))
            {
                filters += " AND a.NamaAgenda LIKE @nama";
                pars.Add(new SqlParameter("@nama", SqlDbType.NVarChar, 255) { Value = "%" + nama.Trim() + "%" });
            }

            string q = FamCte + @",
				Aktif AS (
					SELECT f.RootID, MAX(fa.TanggalSelesai) AS TanggalSelesai
					FROM Fam f
					INNER JOIN ISEE_PNP_AgendaUjian fa ON fa.ID = f.AgendaID
					WHERE fa.Status = 'Aktif'
					  AND fa.TanggalSelesai >= CAST(GETDATE() AS DATE)
					GROUP BY f.RootID
				),
				Roots AS (
					SELECT a.ID, a.NamaAgenda, a.KelompokDistribusiID, a.TanggalMulai,
						   " + PeriodeExpr("a") + @" AS Periode,
						   ak.TanggalSelesai,
						   (SELECT COUNT(*) FROM Fam x WHERE x.RootID = a.ID AND x.AgendaID <> a.ID) AS JumlahSusulan
					FROM ISEE_PNP_AgendaUjian a
					INNER JOIN Aktif ak ON ak.RootID = a.ID
					WHERE a.ParentAgendaID IS NULL" + filters + @"
				)" + PesertaCtes + SelesaiCte + @"
				SELECT r.ID, r.NamaAgenda, r.Periode, kd.NamaKelompok, r.TanggalSelesai,
					   DATEDIFF(DAY, CAST(GETDATE() AS DATE), r.TanggalSelesai) AS SisaHari,
					   ISNULL(tp.Total, 0) AS TotalPeserta,
					   ISNULL(sl.Total, 0) AS Selesai,
					   r.JumlahSusulan
				FROM Roots r
				LEFT JOIN ISEE_PNP_KelompokDistribusi kd ON kd.ID = r.KelompokDistribusiID
				LEFT JOIN TotalPeserta tp ON tp.RootID = r.ID
				LEFT JOIN Selesai sl ON sl.RootID = r.ID
				ORDER BY r.TanggalSelesai ASC, r.ID" + RecursionHint;

            using (var conn = new SqlConnection(_connStr))
            using (var cmd = new SqlCommand(q, conn))
            {
                cmd.Parameters.AddRange(pars.ToArray());
                conn.Open();

                using (var reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        int total = reader.GetInt32(6);
                        int selesai = reader.GetInt32(7);

                        list.Add(new PengerjaanAktifModel
                        {
                            ID = reader.GetInt32(0),
                            NamaAgenda = reader.IsDBNull(1) ? "" : reader.GetString(1),
                            Periode = reader.IsDBNull(2) ? "" : reader.GetString(2),
                            NamaKelompokDistribusi = reader.IsDBNull(3) ? "" : reader.GetString(3),
                            TanggalSelesai = reader.GetDateTime(4),
                            SisaHari = Math.Max(reader.GetInt32(5), 0),
                            TotalPeserta = total,
                            Selesai = selesai,
                            Belum = Math.Max(total - selesai, 0),
                            JumlahSusulan = reader.GetInt32(8)
                        });
                    }
                }
            }

            return list;
        }

        // ======================================================================
        // 4. HASIL JAWABAN AGENDA DISTRIBUSI (susulan ikut)
        // ======================================================================

        public List<HasilJawabanModel> GetHasilJawaban(int agendaId, int? jenisSoalId)
        {
            var list = new List<HasilJawabanModel>();
            var pars = new List<SqlParameter> { new SqlParameter("@agendaId", agendaId) };
            string jenisFilter = "";

            if (jenisSoalId.HasValue && jenisSoalId.Value > 0)
            {
                jenisFilter = " AND bs.JenisSoalID = @jenisSoalId";
                pars.Add(new SqlParameter("@jenisSoalId", jenisSoalId.Value));
            }

            string q = FamCte + JawabanAggCtes + @"
				SELECT js.ID AS JenisSoalID,
					   js.NamaJenisSoal,
					   bs.KategoriSoal,
					   ISNULL(SUM(d.Cnt), 0) AS JumlahDistribusi,
					   ISNULL(SUM(ja.Benar), 0) AS Benar,
					   ISNULL(SUM(ja.Salah), 0) AS Salah
				FROM Dist d
				INNER JOIN ISEE_PNP_BankSoal bs ON bs.ID = d.BankSoalID
				INNER JOIN ISEE_PNP_JenisSoal js ON js.ID = bs.JenisSoalID
				LEFT JOIN JawabAgg ja ON ja.BankSoalID = bs.ID
				WHERE 1 = 1" + jenisFilter + @"
				GROUP BY js.ID, js.NamaJenisSoal, bs.KategoriSoal
				ORDER BY js.NamaJenisSoal, bs.KategoriSoal" + RecursionHint;

            using (var conn = new SqlConnection(_connStr))
            using (var cmd = new SqlCommand(q, conn))
            {
                cmd.Parameters.AddRange(pars.ToArray());
                conn.Open();

                using (var reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        list.Add(new HasilJawabanModel
                        {
                            JenisSoalID = reader.GetInt32(0),
                            NamaJenisSoal = reader.IsDBNull(1) ? "" : reader.GetString(1),
                            KategoriSoal = reader.IsDBNull(2) ? "" : reader.GetString(2),
                            JumlahDistribusi = Convert.ToInt32(reader.GetValue(3)),
                            Benar = Convert.ToInt32(reader.GetValue(4)),
                            Salah = Convert.ToInt32(reader.GetValue(5))
                        });
                    }
                }
            }

            return list;
        }

        public List<HasilJawabanDetailModel> GetHasilJawabanDetail(int agendaId, string kategoriSoal, int? jenisSoalId)
        {
            var list = new List<HasilJawabanDetailModel>();
            var pars = new List<SqlParameter>
            {
                new SqlParameter("@agendaId", agendaId),
                new SqlParameter("@kategoriSoal", SqlDbType.VarChar, 100) { Value = kategoriSoal ?? "" }
            };
            string jenisFilter = "";

            if (jenisSoalId.HasValue && jenisSoalId.Value > 0)
            {
                jenisFilter = " AND bs.JenisSoalID = @jenisSoalId";
                pars.Add(new SqlParameter("@jenisSoalId", jenisSoalId.Value));
            }

            string q = FamCte + JawabanAggCtes + @"
				SELECT bs.ID, bs.Pertanyaan, bs.KunciJawabanLabel,
					   d.Cnt AS JumlahDistribusi,
					   ISNULL(ja.Benar, 0) AS Benar,
					   ISNULL(ja.Salah, 0) AS Salah
				FROM Dist d
				INNER JOIN ISEE_PNP_BankSoal bs ON bs.ID = d.BankSoalID
				LEFT JOIN JawabAgg ja ON ja.BankSoalID = bs.ID
				WHERE bs.KategoriSoal = @kategoriSoal" + jenisFilter + @"
				ORDER BY bs.ID" + RecursionHint;

            using (var conn = new SqlConnection(_connStr))
            using (var cmd = new SqlCommand(q, conn))
            {
                cmd.Parameters.AddRange(pars.ToArray());
                conn.Open();

                using (var reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        list.Add(new HasilJawabanDetailModel
                        {
                            BankSoalID = reader.GetInt32(0),
                            Pertanyaan = reader.IsDBNull(1) ? "" : reader.GetString(1),
                            KunciJawabanLabel = reader.IsDBNull(2) ? "" : reader.GetString(2),
                            JumlahDistribusi = Convert.ToInt32(reader.GetValue(3)),
                            Benar = Convert.ToInt32(reader.GetValue(4)),
                            Salah = Convert.ToInt32(reader.GetValue(5))
                        });
                    }
                }
            }

            return list;
        }

        // ======================================================================
        // 5. MONITORING REALTIME
        // ======================================================================

        public MonitoringPageModel GetMonitoring(int? agendaId, int? kelompokId, string search, int page, int pageSize)
        {
            var result = new MonitoringPageModel();
            page = page < 1 ? 1 : page;
            pageSize = pageSize < 1 ? 15 : pageSize;

            string whereSql = " WHERE 1 = 1";
            var pars = new List<SqlParameter>();

            if (agendaId.HasValue && agendaId.Value > 0)
            {
                // agenda induk -> ikut seluruh peserta agenda susulannya
                whereSql += " AND up.AgendaUjianID IN (SELECT AgendaID FROM Fam WHERE RootID = @agendaId)";
                pars.Add(new SqlParameter("@agendaId", agendaId.Value));
            }

            if (kelompokId.HasValue && kelompokId.Value > 0)
            {
                whereSql += " AND a.KelompokDistribusiID = @kelompokId";
                pars.Add(new SqlParameter("@kelompokId", kelompokId.Value));
            }

            if (!string.IsNullOrWhiteSpace(search))
            {
                whereSql += " AND (staff.Full_Name LIKE @search OR up.DomainID LIKE @search)";
                pars.Add(new SqlParameter("@search", SqlDbType.NVarChar, 200) { Value = "%" + search.Trim() + "%" });
            }

            const string fromSql = @"
				FROM ISEE_PNP_UjianPeserta up
				INNER JOIN ISEE_PNP_AgendaUjian a ON a.ID = up.AgendaUjianID
				LEFT JOIN ISEE_PNP_KelompokDistribusi kd ON kd.ID = a.KelompokDistribusiID
				LEFT JOIN v_ISEE_STAFF_DATA staff ON staff.Domain_ID = up.DomainID";

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

                using (var cmd = new SqlCommand(FamCte + " SELECT COUNT(*) " + fromSql + whereSql + RecursionHint, conn))
                {
                    cmd.Parameters.AddRange(pars.Select(p => Clone(p)).ToArray());
                    result.Total = Convert.ToInt32(cmd.ExecuteScalar());
                }

                string q = FamCte + @"
					SELECT up.ID,
						   ISNULL(staff.Full_Name, up.DomainID) AS NamaPeserta,
						   up.DomainID,
						   a.NamaAgenda + CASE WHEN a.ParentAgendaID IS NOT NULL THEN ' (Susulan)' ELSE '' END AS NamaAgenda,
						   " + PeriodeExpr("a") + @" AS Periode,
						   kd.NamaKelompok,
						   up.MulaiUjian,
						   up.SelesaiUjian,
						   td.DurasiDetik
					" + fromSql + DurasiApply + whereSql + @"
					ORDER BY up.MulaiUjian DESC, up.ID DESC
					OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY" + RecursionHint;

                using (var cmd = new SqlCommand(q, conn))
                {
                    cmd.Parameters.AddRange(pars.Select(p => Clone(p)).ToArray());
                    cmd.Parameters.AddWithValue("@offset", (page - 1) * pageSize);
                    cmd.Parameters.AddWithValue("@pageSize", pageSize);

                    using (var reader = cmd.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            result.Items.Add(new MonitoringRowModel
                            {
                                UjianPesertaID = reader.GetInt32(0),
                                NamaPeserta = reader.IsDBNull(1) ? "-" : reader.GetString(1),
                                DomainID = reader.IsDBNull(2) ? "" : reader.GetString(2),
                                NamaAgenda = reader.IsDBNull(3) ? "" : reader.GetString(3),
                                Periode = reader.IsDBNull(4) ? "" : reader.GetString(4),
                                NamaKelompokDistribusi = reader.IsDBNull(5) ? "" : reader.GetString(5),
                                MulaiUjian = reader.IsDBNull(6) ? (DateTime?)null : reader.GetDateTime(6),
                                SelesaiUjian = reader.IsDBNull(7) ? (DateTime?)null : reader.GetDateTime(7),
                                DurasiDetik = reader.IsDBNull(8) ? (int?)null : Convert.ToInt32(reader.GetValue(8))
                            });
                        }
                    }
                }
            }

            return result;
        }

        public MonitoringDetailModel GetMonitoringDetail(int ujianPesertaId)
        {
            var model = new MonitoringDetailModel();

            string q = @"
				SELECT ISNULL(staff.Full_Name, up.DomainID),
					   up.DomainID,
					   a.NamaAgenda + CASE WHEN a.ParentAgendaID IS NOT NULL THEN ' (Susulan)' ELSE '' END,
					   up.Status,
					   up.Nilai,
					   up.MulaiUjian,
					   up.SelesaiUjian,
					   td.DurasiDetik
				FROM ISEE_PNP_UjianPeserta up
				INNER JOIN ISEE_PNP_AgendaUjian a ON a.ID = up.AgendaUjianID
				LEFT JOIN v_ISEE_STAFF_DATA staff ON staff.Domain_ID = up.DomainID
				" + DurasiApply + @"
				WHERE up.ID = @id;

				SELECT sp.BankSoalID, bs.Pertanyaan, bs.KunciJawabanLabel, jp.JawabanDipilih
				FROM ISEE_PNP_SoalPeserta sp
				INNER JOIN ISEE_PNP_BankSoal bs ON bs.ID = sp.BankSoalID
				LEFT JOIN ISEE_PNP_JawabanPeserta jp
					   ON jp.UjianPesertaID = sp.UjianPesertaID AND jp.BankSoalID = sp.BankSoalID
				WHERE sp.UjianPesertaID = @id
				ORDER BY sp.Urutan, sp.ID;

				SELECT so.BankSoalID AS BankSoalID, so.AssignedLabel AS Label, os.TeksOpsi AS TeksOpsi, so.IsCorrect AS IsCorrect
				FROM ISEE_PNP_SoalPesertaOpsi so
				INNER JOIN ISEE_PNP_OpsiSoal os ON os.ID = so.OpsiSoalID
				WHERE so.UjianPesertaID = @id
				UNION ALL
				SELECT os.BankSoalID, os.LabelOpsi, os.TeksOpsi,
					   CASE WHEN os.LabelOpsi = bs.KunciJawabanLabel THEN 1 ELSE 0 END
				FROM ISEE_PNP_SoalPeserta sp
				INNER JOIN ISEE_PNP_BankSoal bs ON bs.ID = sp.BankSoalID
				INNER JOIN ISEE_PNP_OpsiSoal os ON os.BankSoalID = sp.BankSoalID
				WHERE sp.UjianPesertaID = @id
				  AND NOT EXISTS (SELECT 1 FROM ISEE_PNP_SoalPesertaOpsi x
								  WHERE x.UjianPesertaID = @id AND x.BankSoalID = sp.BankSoalID)
				ORDER BY BankSoalID, Label;";

            var soalDict = new Dictionary<int, MonitoringSoalDetailModel>();
            var orderedBankIds = new List<int>();

            using (var conn = new SqlConnection(_connStr))
            using (var cmd = new SqlCommand(q, conn))
            {
                cmd.Parameters.AddWithValue("@id", ujianPesertaId);
                conn.Open();

                using (var reader = cmd.ExecuteReader())
                {
                    if (reader.Read())
                    {
                        model.Peserta = new MonitoringPesertaInfoModel
                        {
                            NamaPeserta = reader.IsDBNull(0) ? "-" : reader.GetString(0),
                            DomainID = reader.IsDBNull(1) ? "" : reader.GetString(1),
                            NamaAgenda = reader.IsDBNull(2) ? "" : reader.GetString(2),
                            Status = reader.IsDBNull(3) ? "" : reader.GetString(3),
                            Nilai = reader.IsDBNull(4) ? (decimal?)null : reader.GetDecimal(4),
                            MulaiUjian = reader.IsDBNull(5) ? (DateTime?)null : reader.GetDateTime(5),
                            SelesaiUjian = reader.IsDBNull(6) ? (DateTime?)null : reader.GetDateTime(6),
                            DurasiDetik = reader.IsDBNull(7) ? (int?)null : Convert.ToInt32(reader.GetValue(7))
                        };
                    }

                    reader.NextResult();
                    while (reader.Read())
                    {
                        int bankId = reader.GetInt32(0);
                        if (!soalDict.ContainsKey(bankId))
                        {
                            soalDict[bankId] = new MonitoringSoalDetailModel
                            {
                                Pertanyaan = reader.IsDBNull(1) ? "" : reader.GetString(1),
                                KunciJawabanLabel = reader.IsDBNull(2) ? "" : reader.GetString(2),
                                JawabanDipilih = reader.IsDBNull(3) ? null : reader.GetString(3)
                            };
                            orderedBankIds.Add(bankId);
                        }
                    }

                    reader.NextResult();
                    while (reader.Read())
                    {
                        int bankId = reader.GetInt32(0);
                        MonitoringSoalDetailModel soal;
                        if (soalDict.TryGetValue(bankId, out soal))
                        {
                            soal.Opsi.Add(new OpsiSoalModel
                            {
                                LabelOpsi = reader.IsDBNull(1) ? "" : reader.GetString(1),
                                TeksOpsi = reader.IsDBNull(2) ? "" : reader.GetString(2),
                                IsCorrect = !reader.IsDBNull(3) && Convert.ToInt32(reader.GetValue(3)) == 1
                            });
                        }
                    }
                }
            }

            model.SoalList = orderedBankIds.Select(id => soalDict[id]).ToList();

            // Kunci disamakan dengan label yang dilihat peserta.
            foreach (var soal in model.SoalList)
            {
                var kunci = soal.Opsi.FirstOrDefault(o => o.IsCorrect);
                if (kunci != null) soal.KunciJawabanLabel = kunci.LabelOpsi;
            }

            return model;
        }

        // ======================================================================
        // 6. SURVEY (PNP + NON PNP)
        // ======================================================================

        public List<SurveyRowModel> GetSurvey(string tipe, string periode)
        {
            var list = new List<SurveyRowModel>();

            bool includePnp = string.IsNullOrEmpty(tipe) || tipe == "PNP";
            bool includeNonPnp = string.IsNullOrEmpty(tipe) || tipe == "NonPNP";
            bool filterPeriode = !string.IsNullOrEmpty(periode) && periode.Length == 7;

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

                if (includePnp)
                {
                    string qPnp = FamCte + @",
						Roots AS (
							SELECT r.ID, r.NamaAgenda, r.KelompokDistribusiID
							FROM ISEE_PNP_AgendaUjian r
							WHERE r.ParentAgendaID IS NULL
							  AND EXISTS (SELECT 1
										  FROM ISEE_PNP_AgendaSurveyPNP s
										  INNER JOIN Fam f ON f.AgendaID = s.AgendaDistribusiID
										  WHERE f.RootID = r.ID)
						)" + PesertaCtes + @",
						SvPeriode AS (
							SELECT f.RootID, s.Periode
							FROM ISEE_PNP_AgendaSurveyPNP s
							INNER JOIN Fam f ON f.AgendaID = s.AgendaDistribusiID
							WHERE 1 = 1" + (filterPeriode ? " AND s.Periode = @periode" : "") + @"
							GROUP BY f.RootID, s.Periode
						),
						Resp AS (
							SELECT f.RootID,
								   COUNT(DISTINCT UPPER(LTRIM(RTRIM(COALESCE(NULLIF(jsp.DomainID, ''), up.DomainID))))) AS TotalResponden,
								   COUNT(jsp.ID) AS TotalJawaban
							FROM ISEE_PNP_JawabanSurveyPeserta jsp
							INNER JOIN ISEE_PNP_UjianPeserta up ON up.ID = jsp.UjianPesertaID
							INNER JOIN Fam f ON f.AgendaID = up.AgendaUjianID
							GROUP BY f.RootID
						)
						SELECT r.ID, r.NamaAgenda, sp.Periode,
							   ISNULL(tp.Total, 0) AS TotalDiundang,
							   ISNULL(rs.TotalResponden, 0) AS TotalResponden,
							   ISNULL(rs.TotalJawaban, 0) AS TotalJawaban
						FROM SvPeriode sp
						INNER JOIN Roots r ON r.ID = sp.RootID
						LEFT JOIN TotalPeserta tp ON tp.RootID = r.ID
						LEFT JOIN Resp rs ON rs.RootID = r.ID
						ORDER BY sp.Periode DESC, r.NamaAgenda" + RecursionHint;

                    using (var cmd = new SqlCommand(qPnp, conn))
                    {
                        if (filterPeriode)
                            cmd.Parameters.Add(new SqlParameter("@periode", SqlDbType.VarChar, 50) { Value = periode });

                        using (var reader = cmd.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                list.Add(new SurveyRowModel
                                {
                                    AgendaID = reader.GetInt32(0),
                                    NamaAgenda = reader.IsDBNull(1) ? "" : reader.GetString(1),
                                    Tipe = "PNP",
                                    Periode = reader.IsDBNull(2) ? "" : reader.GetString(2),
                                    TotalRespondenDiundang = reader.GetInt32(3),
                                    TotalResponden = reader.GetInt32(4),
                                    TotalJawaban = reader.GetInt32(5)
                                });
                            }
                        }
                    }
                }

                if (includeNonPnp)
                {
                    string qNonPnp = @"
						;WITH G AS (
							SELECT MIN(ID) AS AgendaID, NamaAgenda, StartDate, EndDate
							FROM ISEE_PNP_AgendaSurveyNonPNP
							GROUP BY NamaAgenda, StartDate, EndDate
						)
						SELECT g.AgendaID, g.NamaAgenda, CONVERT(char(7), g.StartDate, 120) AS Periode,
							   ISNULL(resp.TotalResponden, 0) AS TotalResponden,
							   ISNULL(resp.TotalJawaban, 0) AS TotalJawaban
						FROM G g
						OUTER APPLY (
							SELECT COUNT(DISTINCT UPPER(LTRIM(RTRIM(jsp.DomainID)))) AS TotalResponden,
								   COUNT(jsp.ID) AS TotalJawaban
							FROM ISEE_PNP_JawabanSurveyPeserta jsp
							WHERE jsp.AgendaNonPNPID IN (
								SELECT m.ID
								FROM ISEE_PNP_AgendaSurveyNonPNP m
								WHERE m.NamaAgenda = g.NamaAgenda AND m.StartDate = g.StartDate AND m.EndDate = g.EndDate)
						) resp
						WHERE 1 = 1" + (filterPeriode ? " AND CONVERT(char(7), g.StartDate, 120) = @periode" : "") + @"
						ORDER BY g.StartDate DESC, g.NamaAgenda;";

                    using (var cmd = new SqlCommand(qNonPnp, conn))
                    {
                        if (filterPeriode)
                            cmd.Parameters.Add(new SqlParameter("@periode", SqlDbType.VarChar, 7) { Value = periode });

                        using (var reader = cmd.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                list.Add(new SurveyRowModel
                                {
                                    AgendaID = reader.GetInt32(0),
                                    NamaAgenda = reader.IsDBNull(1) ? "" : reader.GetString(1),
                                    Tipe = "NonPNP",
                                    Periode = reader.IsDBNull(2) ? "" : reader.GetString(2),
                                    TotalRespondenDiundang = 0,
                                    TotalResponden = reader.GetInt32(3),
                                    TotalJawaban = reader.GetInt32(4)
                                });
                            }
                        }
                    }
                }
            }

            return list.OrderByDescending(r => r.Periode).ThenBy(r => r.NamaAgenda).ToList();
        }

        private const string SurveyDetailSelect = @"
			SELECT j.PertanyaanID,
				   COALESCE(MAX(pt.PertanyaanText),
					 MAX(ps.NamaPertanyaan),
					 N'Pertanyaan #' + CAST(j.PertanyaanID AS nvarchar(20))) AS NamaPertanyaan,
				   COUNT(DISTINCT j.K) AS JumlahDijawab,
				   t.Total AS TotalResponden,
				   AVG(CAST(j.Nilai AS float)) AS RataNilai
			FROM Jwb j
			CROSS JOIN TotalR t
			LEFT JOIN ISEE_PNP_PertanyaanSurvey ps
				   ON ps.ID = j.PertanyaanID
				  AND j.PertanyaanID IN (SELECT PertanyaanID FROM Grp)
			LEFT JOIN ISEE_PNP_PertanyaanTemplate pt ON pt.ID = j.PertanyaanID
			GROUP BY j.PertanyaanID, t.Total
			ORDER BY j.PertanyaanID;";

        public SurveyDetailModel GetSurveyDetail(int agendaId, string tipe)
        {
            var model = new SurveyDetailModel();
            bool nonPnp = (tipe == "NonPNP");

            string q = nonPnp
                ? @"
				DECLARE @nama nvarchar(500), @s date, @e date;
				SELECT @nama = NamaAgenda, @s = StartDate, @e = EndDate
				FROM ISEE_PNP_AgendaSurveyNonPNP WHERE ID = @id;

				SELECT @nama AS NamaAgenda;

				;WITH Grp AS (
					SELECT ID, PertanyaanID
					FROM ISEE_PNP_AgendaSurveyNonPNP
					WHERE NamaAgenda = @nama AND StartDate = @s AND EndDate = @e
				),
				Jwb AS (
					SELECT jsp.PertanyaanID, jsp.Nilai, UPPER(LTRIM(RTRIM(jsp.DomainID))) AS K
					FROM ISEE_PNP_JawabanSurveyPeserta jsp
					WHERE jsp.AgendaNonPNPID IN (SELECT ID FROM Grp)
				),
				TotalR AS (SELECT COUNT(DISTINCT K) AS Total FROM Jwb)"
								+ SurveyDetailSelect
								: @"
				SELECT NamaAgenda FROM ISEE_PNP_AgendaUjian WHERE ID = @id;
				"
								+ FamCte + @",
				Grp AS (
					SELECT s.PertanyaanID
					FROM ISEE_PNP_AgendaSurveyPNP s
					INNER JOIN Fam f ON f.AgendaID = s.AgendaDistribusiID
					WHERE f.RootID = @id
				),
				Jwb AS (
					SELECT jsp.PertanyaanID, jsp.Nilai,
						   UPPER(LTRIM(RTRIM(COALESCE(NULLIF(jsp.DomainID, ''), up.DomainID)))) AS K
					FROM ISEE_PNP_JawabanSurveyPeserta jsp
					INNER JOIN ISEE_PNP_UjianPeserta up ON up.ID = jsp.UjianPesertaID
					INNER JOIN Fam f ON f.AgendaID = up.AgendaUjianID
					WHERE f.RootID = @id
				),
				TotalR AS (SELECT COUNT(DISTINCT K) AS Total FROM Jwb)"
                + SurveyDetailSelect;

            using (var conn = new SqlConnection(_connStr))
            using (var cmd = new SqlCommand(q, conn))
            {
                cmd.Parameters.AddWithValue("@id", agendaId);
                conn.Open();

                using (var reader = cmd.ExecuteReader())
                {
                    model.NamaAgenda = reader.Read() && !reader.IsDBNull(0) ? reader.GetString(0) : "";

                    reader.NextResult();
                    while (reader.Read())
                    {
                        int totalResponden = Convert.ToInt32(reader.GetValue(3));
                        model.TotalResponden = totalResponden;

                        model.Breakdown.Add(new SurveyBreakdownItemModel
                        {
                            NamaPertanyaan = reader.IsDBNull(1) ? "" : reader.GetString(1),
                            JumlahDijawab = Convert.ToInt32(reader.GetValue(2)),
                            TotalJawaban = totalResponden,
                            RataNilai = reader.IsDBNull(4) ? (double?)null : Convert.ToDouble(reader.GetValue(4))
                        });
                    }
                }
            }

            return model;
        }

        private static SqlParameter Clone(SqlParameter p)
        {
            var c = new SqlParameter(p.ParameterName, p.SqlDbType) { Value = p.Value };
            if (p.Size > 0) c.Size = p.Size;
            return c;
        }
    }
}

← Back to timeline