using System; using System.Collections.Generic; using System.Configuration; using System.Data.SqlClient; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Web; using System.Web.Mvc; using System.IO; using InstaVibe.Models; namespace InstaVibe.Controllers { public class ComplaintController : Controller { private string connectionString = ConfigurationManager .ConnectionStrings["InstaVibe"].ConnectionString; private void EnsureComplaintSchema(SqlConnection con) { // Columns on Complaints using (var cmd = new SqlCommand(@" IF COL_LENGTH('Complaints','student_name') IS NULL ALTER TABLE Complaints ADD student_name VARCHAR(100) NULL; IF COL_LENGTH('Complaints','user_id') IS NULL ALTER TABLE Complaints ADD user_id INT NULL; IF COL_LENGTH('Complaints','status') IS NULL ALTER TABLE Complaints ADD status VARCHAR(30) NULL CONSTRAINT DF_Complaints_Status DEFAULT('Submitted'); IF COL_LENGTH('Complaints','submitted_at') IS NULL ALTER TABLE Complaints ADD submitted_at DATETIME NULL CONSTRAINT DF_Complaints_SubmittedAt DEFAULT(GETDATE()); -- Attachments IF OBJECT_ID('dbo.ComplaintAttachments','U') IS NULL BEGIN CREATE TABLE dbo.ComplaintAttachments ( attachment_id INT IDENTITY(1,1) PRIMARY KEY, complaint_id INT NOT NULL, file_url VARCHAR(400) NOT NULL, mime_type VARCHAR(100) NULL, created_at DATETIME DEFAULT GETDATE() ); END -- Messages IF OBJECT_ID('dbo.ComplaintMessages','U') IS NULL BEGIN CREATE TABLE dbo.ComplaintMessages ( message_id INT IDENTITY(1,1) PRIMARY KEY, complaint_id INT NOT NULL, sender VARCHAR(20) NOT NULL, message VARCHAR(1000) NOT NULL, is_read_by_student BIT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT GETDATE() ); END ", con)) { cmd.ExecuteNonQuery(); } } // ======= USER: Complaint Form Page ======= public ActionResult Index() { return View(); } // ======= USER: Submit Complaint (Anonymous) ======= [HttpPost] public JsonResult Submit(string category, string message, string studentName, bool personalized = false) { if (string.IsNullOrWhiteSpace(message) || string.IsNullOrWhiteSpace(category)) return Json(new { success = false, message = "Category aur complaint dono required hain." }); if (message.Length > 1000) return Json(new { success = false, message = "Complaint 1000 characters se zyada nahi ho sakti." }); // Spam control: same IP se 24 ghante mein sirf 3 complaints string ipHash = GetIpHash(); int? userId = null; string autoStudentName = ""; if (Session["UserId"] != null) { int parsed; if (int.TryParse(Session["UserId"].ToString(), out parsed)) userId = parsed; } try { using (SqlConnection con = new SqlConnection(connectionString)) { con.Open(); EnsureComplaintSchema(con); if (!userId.HasValue && Session["Username"] != null) { using (var uidCmd = new SqlCommand("SELECT TOP 1 user_id FROM Users WHERE username=@u", con)) { uidCmd.Parameters.AddWithValue("@u", Session["Username"].ToString()); object uidObj = uidCmd.ExecuteScalar(); if (uidObj != null && uidObj != DBNull.Value) { userId = Convert.ToInt32(uidObj); } } } // Logged-in user complaints: always resolve student's name reliably. if (userId.HasValue) { if (Session["FullName"] != null) { autoStudentName = (Session["FullName"].ToString() ?? "").Trim(); } if (string.IsNullOrWhiteSpace(autoStudentName) && Session["Username"] != null) { autoStudentName = (Session["Username"].ToString() ?? "").Trim(); } if (string.IsNullOrWhiteSpace(autoStudentName)) { using (var u = new SqlCommand("SELECT fullname, username FROM Users WHERE user_id=@uid", con)) { u.Parameters.AddWithValue("@uid", userId.Value); using (var r = u.ExecuteReader()) { if (r.Read()) { autoStudentName = (r["fullname"] == DBNull.Value ? "" : r["fullname"].ToString()).Trim(); if (string.IsNullOrWhiteSpace(autoStudentName)) autoStudentName = (r["username"] == DBNull.Value ? "" : r["username"].ToString()).Trim(); } } } } } string spamCheck = @"SELECT COUNT(*) FROM Complaints WHERE ip_hash = @ipHash AND submitted_at >= DATEADD(HOUR, -24, GETDATE())"; using (SqlCommand cmd = new SqlCommand(spamCheck, con)) { cmd.Parameters.AddWithValue("@ipHash", ipHash); int count = (int)cmd.ExecuteScalar(); if (count >= 3) return Json(new { success = false, message = "Aap 24 ghante mein sirf 3 complaints submit kar sakte hain." }); } bool hasStudentName = true; bool hasUserId = true; bool hasStatus = true; bool hasSubmittedAt = true; // Build insert dynamically based on existing columns var cols = new System.Collections.Generic.List { "category", "message", "ip_hash" }; var vals = new System.Collections.Generic.List { "@category", "@message", "@ipHash" }; if (hasStudentName) { cols.Add("student_name"); vals.Add("@studentName"); } if (hasUserId) { cols.Add("user_id"); vals.Add("@userId"); } if (hasStatus) { cols.Add("status"); vals.Add("@status"); } if (hasSubmittedAt) { cols.Add("submitted_at"); vals.Add("GETDATE()"); } string insert = $"INSERT INTO Complaints ({string.Join(",", cols)}) VALUES ({string.Join(",", vals)}); SELECT SCOPE_IDENTITY();"; int complaintId; using (SqlCommand cmd = new SqlCommand(insert, con)) { cmd.Parameters.AddWithValue("@category", category); cmd.Parameters.AddWithValue("@message", message); cmd.Parameters.AddWithValue("@ipHash", ipHash); if (hasStudentName) cmd.Parameters.AddWithValue("@studentName", (object)autoStudentName); if (hasUserId) cmd.Parameters.AddWithValue("@userId", (object)userId ?? DBNull.Value); if (hasStatus) cmd.Parameters.AddWithValue("@status", "Submitted"); complaintId = Convert.ToInt32(cmd.ExecuteScalar()); } // Save attachments (if ComplaintAttachments table exists) if (Request.Files != null && Request.Files.Count > 0) { string uploadRel = "~/Uploads/Complaints/"; string uploadAbs = Server.MapPath(uploadRel); if (!Directory.Exists(uploadAbs)) Directory.CreateDirectory(uploadAbs); for (int i = 0; i < Request.Files.Count; i++) { var file = Request.Files[i]; if (file == null || file.ContentLength <= 0) continue; string ext = Path.GetExtension(file.FileName) ?? ""; string safeName = Guid.NewGuid().ToString("N") + ext; string absPath = Path.Combine(uploadAbs, safeName); file.SaveAs(absPath); string relUrl = Url.Content(uploadRel + safeName); string mime = file.ContentType ?? ""; using (SqlCommand a = new SqlCommand( @"INSERT INTO ComplaintAttachments (complaint_id, file_url, mime_type, created_at) VALUES (@cid, @url, @mime, GETDATE())", con)) { a.Parameters.AddWithValue("@cid", complaintId); a.Parameters.AddWithValue("@url", relUrl); a.Parameters.AddWithValue("@mime", mime); a.ExecuteNonQuery(); } } } } } catch { return Json(new { success = false, message = "An error occurred while submitting your complaint. Please try again later." }); } return Json(new { success = true, message = "Aapki complaint submit ho gayi. Shukriya!" }); } // ======= ADMIN: Get All Complaints ======= public JsonResult GetAll() { if (!IsAuthorized("complaints")) return Json(new { error = "Unauthorized" }, JsonRequestBehavior.AllowGet); var list = new List(); using (SqlConnection con = new SqlConnection(connectionString)) { con.Open(); EnsureComplaintSchema(con); bool hasStudentName = true; bool hasStatus = true; bool hasSubmittedAt = true; bool hasAttachTable = true; string query = @"SELECT C.complaint_id, C.category, C.message" + (hasStudentName ? ", C.student_name" : "") + (hasStatus ? ", C.status" : "") + (hasSubmittedAt ? ", C.submitted_at" : "") + (hasAttachTable ? ", (SELECT COUNT(*) FROM ComplaintAttachments A WHERE A.complaint_id=C.complaint_id) AS attachment_count" : "") + @", U.fullname AS fallback_fullname, U.username AS fallback_username FROM Complaints C LEFT JOIN Users U ON U.user_id = C.user_id ORDER BY " + (hasSubmittedAt ? "C.submitted_at" : "C.complaint_id") + " DESC"; using (SqlCommand cmd = new SqlCommand(query, con)) { var reader = cmd.ExecuteReader(); while (reader.Read()) { int cid = Convert.ToInt32(reader["complaint_id"]); int attachCount = hasAttachTable ? Convert.ToInt32(reader["attachment_count"]) : 0; string studentName = hasStudentName ? (reader["student_name"] == DBNull.Value ? "" : reader["student_name"].ToString()) : ""; if (string.IsNullOrWhiteSpace(studentName)) { studentName = reader["fallback_fullname"] == DBNull.Value ? "" : reader["fallback_fullname"].ToString(); } if (string.IsNullOrWhiteSpace(studentName)) { studentName = reader["fallback_username"] == DBNull.Value ? "" : reader["fallback_username"].ToString(); } list.Add(new { ComplaintId = reader["complaint_id"], Category = reader["category"].ToString(), Message = reader["message"].ToString(), StudentName = studentName, Status = hasStatus ? (reader["status"] == DBNull.Value ? "Submitted" : reader["status"].ToString()) : "Submitted", Attachments = attachCount, SubmittedAt = hasSubmittedAt ? ((DateTime)reader["submitted_at"]).ToString("dd MMM yyyy, hh:mm tt") : "" }); } } } return Json(list, JsonRequestBehavior.AllowGet); } // ======= ADMIN: Update Status ======= [HttpPost] public JsonResult UpdateStatus(int complaintId, string status) { if (!IsAuthorized("complaints")) return Json(new { success = false, message = "Unauthorized" }); status = (status ?? "").Trim(); var allowed = new[] { "Submitted", "In Progress", "Resolved" }; if (!allowed.Contains(status)) status = "Submitted"; using (SqlConnection con = new SqlConnection(connectionString)) { con.Open(); EnsureComplaintSchema(con); int? uid = GetComplaintUserId(con, complaintId); using (var cmd = new SqlCommand("UPDATE Complaints SET status=@s WHERE complaint_id=@id", con)) { cmd.Parameters.AddWithValue("@s", status); cmd.Parameters.AddWithValue("@id", complaintId); cmd.ExecuteNonQuery(); } if (uid.HasValue) { NotifyStudent(uid.Value, $"Admin updated your complaint #{complaintId} status to: {status}"); } } return Json(new { success = true }); } // ======= ADMIN: Send Message to Student (thread) ======= [HttpPost] public JsonResult AdminMessage(int complaintId, string message) { if (!IsAuthorized("complaints")) return Json(new { success = false, message = "Unauthorized" }); message = (message ?? "").Trim(); if (string.IsNullOrWhiteSpace(message)) return Json(new { success = false, message = "Message required." }); using (SqlConnection con = new SqlConnection(connectionString)) { con.Open(); EnsureComplaintSchema(con); int? uid = GetComplaintUserId(con, complaintId); using (var cmd = new SqlCommand( @"INSERT INTO ComplaintMessages (complaint_id, sender, message, is_read_by_student, created_at) VALUES (@cid,'Admin',@msg,0,GETDATE())", con)) { cmd.Parameters.AddWithValue("@cid", complaintId); cmd.Parameters.AddWithValue("@msg", message); cmd.ExecuteNonQuery(); } if (uid.HasValue) { NotifyStudent(uid.Value, $"Admin asked about your complaint #{complaintId}: {message}"); } } return Json(new { success = true }); } public JsonResult GetThread(int complaintId) { if (!IsAuthorized("complaints")) return Json(new { error = "Unauthorized" }, JsonRequestBehavior.AllowGet); var list = new List(); using (SqlConnection con = new SqlConnection(connectionString)) { con.Open(); EnsureComplaintSchema(con); using (var cmd = new SqlCommand( @"SELECT sender, message, created_at FROM ComplaintMessages WHERE complaint_id=@cid ORDER BY created_at ASC", con)) { cmd.Parameters.AddWithValue("@cid", complaintId); using (var r = cmd.ExecuteReader()) { while (r.Read()) { list.Add(new { Sender = r["sender"].ToString(), Message = r["message"].ToString(), Time = r["created_at"] == DBNull.Value ? "" : Convert.ToDateTime(r["created_at"]).ToString("dd MMM yyyy, hh:mm tt") }); } } } } return Json(list, JsonRequestBehavior.AllowGet); } // ======= Shared: Get Attachments ======= public JsonResult GetAttachments(int complaintId) { // allow admin/subadmin OR the owning student (if user_id exists) bool isAdmin = IsAuthorized("complaints"); using (SqlConnection con = new SqlConnection(connectionString)) { con.Open(); EnsureComplaintSchema(con); if (!isAdmin && HasColumn(con, "Complaints", "user_id")) { if (Session["UserId"] == null) return Json(new { error = "Unauthorized" }, JsonRequestBehavior.AllowGet); int uid = Convert.ToInt32(Session["UserId"]); using (var own = new SqlCommand("SELECT COUNT(*) FROM Complaints WHERE complaint_id=@id AND user_id=@uid", con)) { own.Parameters.AddWithValue("@id", complaintId); own.Parameters.AddWithValue("@uid", uid); if (Convert.ToInt32(own.ExecuteScalar()) == 0) return Json(new { error = "Unauthorized" }, JsonRequestBehavior.AllowGet); } } var list = new List(); using (var cmd = new SqlCommand( @"SELECT attachment_id, file_url, mime_type, created_at FROM ComplaintAttachments WHERE complaint_id=@id ORDER BY created_at DESC", con)) { cmd.Parameters.AddWithValue("@id", complaintId); using (var r = cmd.ExecuteReader()) { while (r.Read()) { list.Add(new { Url = r["file_url"].ToString(), Mime = r["mime_type"] == DBNull.Value ? "" : r["mime_type"].ToString() }); } } } return Json(list, JsonRequestBehavior.AllowGet); } } // ======= STUDENT: Updates count for complaint dropdown ======= public JsonResult GetMyUpdatesCount() { if (Session["UserId"] == null) return Json(0, JsonRequestBehavior.AllowGet); int uid = Convert.ToInt32(Session["UserId"]); using (SqlConnection con = new SqlConnection(connectionString)) { con.Open(); EnsureComplaintSchema(con); string q = @"SELECT COUNT(*) FROM ComplaintMessages M JOIN Complaints C ON C.complaint_id = M.complaint_id WHERE C.user_id=@uid AND M.sender='Admin' AND M.is_read_by_student=0"; using (var cmd = new SqlCommand(q, con)) { cmd.Parameters.AddWithValue("@uid", uid); return Json(Convert.ToInt32(cmd.ExecuteScalar()), JsonRequestBehavior.AllowGet); } } } // ======= STUDENT: List updates + allow reply ======= public JsonResult GetMyUpdates() { if (Session["UserId"] == null) return Json(new { error = "Unauthorized" }, JsonRequestBehavior.AllowGet); int uid = Convert.ToInt32(Session["UserId"]); using (SqlConnection con = new SqlConnection(connectionString)) { con.Open(); EnsureComplaintSchema(con); bool hasStatus = true; bool hasSubmittedAt = true; bool hasMsgTable = true; string q = @"SELECT TOP 20 complaint_id, category, message" + (hasStatus ? ", status" : "") + (hasSubmittedAt ? ", submitted_at" : "") + (hasMsgTable ? @", (SELECT TOP 1 message FROM ComplaintMessages WHERE complaint_id=Complaints.complaint_id AND sender='Admin' ORDER BY created_at DESC) AS last_admin_message" : "") + @" FROM Complaints WHERE user_id=@uid ORDER BY " + (hasSubmittedAt ? "submitted_at" : "complaint_id") + " DESC"; var list = new List(); using (var cmd = new SqlCommand(q, con)) { cmd.Parameters.AddWithValue("@uid", uid); using (var r = cmd.ExecuteReader()) { while (r.Read()) { int cid = Convert.ToInt32(r["complaint_id"]); string lastAdmin = hasMsgTable ? (r["last_admin_message"] == DBNull.Value ? "" : r["last_admin_message"].ToString()) : ""; list.Add(new { ComplaintId = cid, Category = r["category"].ToString(), Complaint = r["message"].ToString(), Status = hasStatus ? (r["status"] == DBNull.Value ? "Submitted" : r["status"].ToString()) : "Submitted", SubmittedAt = hasSubmittedAt ? ((DateTime)r["submitted_at"]).ToString("dd MMM yyyy, hh:mm tt") : "", LastAdminMessage = lastAdmin }); } } } // mark all admin messages as read (when student opens updates) if (hasMsgTable) { using (var up = new SqlCommand( @"UPDATE M SET is_read_by_student=1 FROM ComplaintMessages M JOIN Complaints C ON C.complaint_id=M.complaint_id WHERE C.user_id=@uid AND M.sender='Admin' AND M.is_read_by_student=0", con)) { up.Parameters.AddWithValue("@uid", uid); up.ExecuteNonQuery(); } } return Json(list, JsonRequestBehavior.AllowGet); } } [HttpPost] public JsonResult Reply(int complaintId, string message) { if (Session["UserId"] == null) return Json(new { success = false, message = "Unauthorized" }); int uid = Convert.ToInt32(Session["UserId"]); message = (message ?? "").Trim(); if (string.IsNullOrWhiteSpace(message)) return Json(new { success = false, message = "Message required." }); using (SqlConnection con = new SqlConnection(connectionString)) { con.Open(); EnsureComplaintSchema(con); using (var own = new SqlCommand("SELECT COUNT(*) FROM Complaints WHERE complaint_id=@id AND user_id=@uid", con)) { own.Parameters.AddWithValue("@id", complaintId); own.Parameters.AddWithValue("@uid", uid); if (Convert.ToInt32(own.ExecuteScalar()) == 0) return Json(new { success = false, message = "Unauthorized" }); } using (var cmd = new SqlCommand( @"INSERT INTO ComplaintMessages (complaint_id, sender, message, is_read_by_student, created_at) VALUES (@cid,'Student',@msg,1,GETDATE())", con)) { cmd.Parameters.AddWithValue("@cid", complaintId); cmd.Parameters.AddWithValue("@msg", message); cmd.ExecuteNonQuery(); } } return Json(new { success = true }); } [HttpPost] public JsonResult Acknowledge(int complaintId) { if (Session["UserId"] == null) return Json(new { success = false, message = "Unauthorized" }); int uid = Convert.ToInt32(Session["UserId"]); using (SqlConnection con = new SqlConnection(connectionString)) { con.Open(); EnsureComplaintSchema(con); using (var own = new SqlCommand("SELECT COUNT(*) FROM Complaints WHERE complaint_id=@id AND user_id=@uid", con)) { own.Parameters.AddWithValue("@id", complaintId); own.Parameters.AddWithValue("@uid", uid); if (Convert.ToInt32(own.ExecuteScalar()) == 0) return Json(new { success = false, message = "Unauthorized" }); } using (var up = new SqlCommand( @"UPDATE ComplaintMessages SET is_read_by_student=1 WHERE complaint_id=@cid AND sender='Admin' AND is_read_by_student=0", con)) { up.Parameters.AddWithValue("@cid", complaintId); up.ExecuteNonQuery(); } // Keep a small audit trail in thread that student has seen the update. using (var cmd = new SqlCommand( @"INSERT INTO ComplaintMessages (complaint_id, sender, message, is_read_by_student, created_at) VALUES (@cid,'Student','Acknowledged. I have reviewed your update.',1,GETDATE())", con)) { cmd.Parameters.AddWithValue("@cid", complaintId); cmd.ExecuteNonQuery(); } } return Json(new { success = true }); } private static bool TableExists(SqlConnection con, string tableName) { using (var cmd = new SqlCommand("SELECT CASE WHEN OBJECT_ID(@t,'U') IS NULL THEN 0 ELSE 1 END", con)) { cmd.Parameters.AddWithValue("@t", "dbo." + tableName); return Convert.ToInt32(cmd.ExecuteScalar()) == 1; } } private static bool HasColumn(SqlConnection con, string table, string column) { using (var cmd = new SqlCommand("SELECT CASE WHEN COL_LENGTH(@t,@c) IS NULL THEN 0 ELSE 1 END", con)) { cmd.Parameters.AddWithValue("@t", table); cmd.Parameters.AddWithValue("@c", column); return Convert.ToInt32(cmd.ExecuteScalar()) == 1; } } // ======= ADMIN: Mark Resolved / Delete ======= [HttpPost] public JsonResult Delete(int complaintId) { if (!IsAuthorized("complaints")) return Json(new { success = false, message = "Unauthorized" }); using (SqlConnection con = new SqlConnection(connectionString)) { con.Open(); int? uid = GetComplaintUserId(con, complaintId); // cleanup child tables if exist if (TableExists(con, "ComplaintAttachments")) { using (var cmd = new SqlCommand("DELETE FROM ComplaintAttachments WHERE complaint_id=@id", con)) { cmd.Parameters.AddWithValue("@id", complaintId); cmd.ExecuteNonQuery(); } } if (TableExists(con, "ComplaintMessages")) { using (var cmd = new SqlCommand("DELETE FROM ComplaintMessages WHERE complaint_id=@id", con)) { cmd.Parameters.AddWithValue("@id", complaintId); cmd.ExecuteNonQuery(); } } string query = "DELETE FROM Complaints WHERE complaint_id = @id"; using (SqlCommand cmd = new SqlCommand(query, con)) { cmd.Parameters.AddWithValue("@id", complaintId); cmd.ExecuteNonQuery(); } if (uid.HasValue) { NotifyStudent(uid.Value, $"Admin deleted your complaint #{complaintId}."); } } return Json(new { success = true }); } private int? GetComplaintUserId(SqlConnection con, int complaintId) { if (!HasColumn(con, "Complaints", "user_id")) return null; using (var cmd = new SqlCommand("SELECT user_id FROM Complaints WHERE complaint_id=@id", con)) { cmd.Parameters.AddWithValue("@id", complaintId); object v = cmd.ExecuteScalar(); if (v == null || v == DBNull.Value) return null; return Convert.ToInt32(v); } } private void NotifyStudent(int userId, string msg) { // Uses existing Notifications table. We set sender=receiver so join with Users works. try { DatabaseHelper.AddNotification(userId, userId, "complaint", msg); } catch { // ignore notification failure } } // ======= Spam Control: IP ko hash karo (identity nahi pata chalti) ======= private string GetIpHash() { string ip = Request.UserHostAddress ?? "unknown"; using (SHA256 sha = SHA256.Create()) { byte[] bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(ip + "salt_instavibe")); return BitConverter.ToString(bytes).Replace("-", "").ToLower(); } } // ======= HELPER: Authorization Check ======= private bool IsAuthorized(string requiredRole) { string adminType = Session["AdminType"]?.ToString(); if (adminType == "SuperAdmin") return true; if (adminType == "SubAdmin") { string subRole = Session["SubAdminRole"]?.ToString() ?? ""; var roles = subRole.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(r => r.Trim()) .Where(r => !string.IsNullOrEmpty(r)) .ToHashSet(StringComparer.OrdinalIgnoreCase); return roles.Contains(requiredRole); } return false; } } }