// Web/Areas/Admin/Controllers/SurveyAnalysisController.cs using Data; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Model; using Services.AIViewModel; using Services.Interaces; using System.Text; using System.Text.Json; using Web.Authorization; namespace Web.Areas.Admin.Controllers { [Area("Admin")] public class SurveyAnalysisController : Controller { private readonly IAiAnalysisService _aiAnalysisService; private readonly SurveyContext _context; private readonly ILogger _logger; public SurveyAnalysisController( IAiAnalysisService aiAnalysisService, SurveyContext context, ILogger logger) { _aiAnalysisService = aiAnalysisService; _context = context; _logger = logger; } #region Dashboard and Overview /// /// Main dashboard showing all questionnaires available for analysis /// public async Task Index() { try { var responses = await _context.Responses .Include(r => r.Questionnaire) .ThenInclude(q => q.Questions) .Include(r => r.ResponseDetails) .ThenInclude(rd => rd.ResponseAnswers) .OrderByDescending(r => r.SubmissionDate) .ToListAsync(); var result = responses.Select(r => new { ResponseId = r.Id, QuestionnaireId = r.QuestionnaireId, Title = r.Questionnaire?.Title ?? "Unknown", Description = r.Questionnaire?.Description ?? "", QuestionCount = r.Questionnaire?.Questions?.Count ?? 0, AnalyzableCount = r.ResponseDetails.Count(rd => !string.IsNullOrEmpty(rd.TextResponse) || rd.ResponseAnswers.Any()), TotalAnswered = r.ResponseDetails.Count, SubmissionDate = r.SubmissionDate, UserName = r.UserName ?? "Anonymous", UserEmail = r.UserEmail ?? "No email available" }).ToList(); ViewBag.ServiceHealth = await _aiAnalysisService.GetServiceHealthStatusAsync(); return View(result); } catch (Exception ex) { _logger.LogError(ex, "Error loading survey analysis dashboard"); TempData["ErrorMessage"] = "Error loading dashboard. Please try again."; return View(new List()); } } /// /// Generate comprehensive analysis overview for a questionnaire /// [HasPermission(Permissions.SurveyAnalysis.Analyze)] public async Task AnalyzeQuestionnaire(int id) { try { var questionnaire = await _context.Questionnaires .Include(q => q.Questions) .FirstOrDefaultAsync(q => q.Id == id); if (questionnaire == null) { TempData["ErrorMessage"] = "Questionnaire not found."; return RedirectToAction(nameof(Index)); } // Check if there are ANY responses (not just text) var hasResponses = await _context.Responses .Include(r => r.ResponseDetails) .ThenInclude(rd => rd.ResponseAnswers) .AnyAsync(r => r.QuestionnaireId == id && r.ResponseDetails.Any(rd => !string.IsNullOrEmpty(rd.TextResponse) || rd.ResponseAnswers.Any())); if (!hasResponses) { TempData["WarningMessage"] = "No analyzable responses found for this questionnaire."; return RedirectToAction(nameof(Index)); } _logger.LogInformation("Starting analysis for questionnaire {QuestionnaireId}", id); var analysisOverview = await _aiAnalysisService.GenerateQuestionnaireOverviewAsync(id); _logger.LogInformation("Analysis completed successfully for questionnaire {QuestionnaireId}", id); return View(analysisOverview); } catch (Exception ex) { _logger.LogError(ex, "Error analyzing questionnaire {QuestionnaireId}: {ErrorMessage}", id, ex.Message); TempData["ErrorMessage"] = $"Error analyzing questionnaire: {ex.Message}"; return RedirectToAction(nameof(Index)); } } #endregion #region High-Risk Response Management /// /// Identify and display high-risk responses requiring immediate attention /// [HasPermission(Permissions.SurveyAnalysis.HighRisk)] public async Task HighRiskResponses(int id) { try { var questionnaire = await _context.Questionnaires .FirstOrDefaultAsync(q => q.Id == id); if (questionnaire == null) { TempData["ErrorMessage"] = "Questionnaire not found."; return RedirectToAction(nameof(Index)); } var highRiskResponses = await _aiAnalysisService.IdentifyHighRiskResponsesAsync(id); ViewBag.QuestionnaireName = questionnaire.Title; ViewBag.QuestionnaireId = id; return View(highRiskResponses); } catch (Exception ex) { _logger.LogError(ex, "Error identifying high-risk responses for questionnaire {QuestionnaireId}", id); TempData["ErrorMessage"] = "Error identifying high-risk responses. Please try again."; return RedirectToAction(nameof(Index)); } } /// /// View detailed analysis of a specific high-risk response /// [HasPermission(Permissions.SurveyAnalysis.HighRisk)] public async Task ViewHighRiskResponse(int questionnaireId, int responseId) { try { var response = await _context.Responses .Include(r => r.Questionnaire) .Include(r => r.ResponseDetails) .ThenInclude(rd => rd.Question) .Include(r => r.ResponseDetails) .ThenInclude(rd => rd.ResponseAnswers) .ThenInclude(ra => ra.Answer) .FirstOrDefaultAsync(r => r.Id == responseId && r.QuestionnaireId == questionnaireId); if (response == null) { TempData["ErrorMessage"] = "Response not found."; return RedirectToAction(nameof(HighRiskResponses), new { id = questionnaireId }); } var analysisResults = new List(); foreach (var detail in response.ResponseDetails) { var analysisText = BuildResponseText(detail); if (!string.IsNullOrWhiteSpace(analysisText)) { var analysisRequest = new AnalysisRequest { ResponseId = response.Id, QuestionId = detail.QuestionId, ResponseText = analysisText, QuestionText = detail.Question?.Text ?? "" }; var analysis = await _aiAnalysisService.AnalyzeCompleteResponseAsync(analysisRequest); analysisResults.Add(analysis); } } ViewBag.Response = response; return View(analysisResults); } catch (Exception ex) { _logger.LogError(ex, "Error viewing high-risk response {ResponseId}", responseId); TempData["ErrorMessage"] = "Error loading response details. Please try again."; return RedirectToAction(nameof(HighRiskResponses), new { id = questionnaireId }); } } #endregion #region Individual Response Analysis /// /// Analyze a single response in detail /// [HttpPost] public async Task AnalyzeResponse(int responseId, int questionId, string responseText, string questionText) { try { var analysisRequest = new AnalysisRequest { ResponseId = responseId, QuestionId = questionId, ResponseText = responseText, QuestionText = questionText }; var isValid = await _aiAnalysisService.ValidateAnalysisRequestAsync(analysisRequest); if (!isValid) return Json(new { success = false, message = "Invalid analysis request." }); var analysis = await _aiAnalysisService.AnalyzeCompleteResponseAsync(analysisRequest); return Json(new { success = true, analysis = new { sentiment = analysis.SentimentAnalysis, keyPhrases = analysis.KeyPhrases?.KeyPhrases ?? new List(), riskLevel = analysis.RiskAssessment?.RiskLevel.ToString(), riskScore = analysis.RiskAssessment?.RiskScore ?? 0, requiresAttention = analysis.RiskAssessment?.RequiresImmediateAttention ?? false, recommendedAction = analysis.RiskAssessment?.RecommendedAction ?? "", insights = analysis.Insights.Select(i => new { category = i.Category, issue = i.Issue, intervention = i.RecommendedIntervention, priority = i.Priority }) } }); } catch (Exception ex) { _logger.LogError(ex, "Error analyzing response {ResponseId}", responseId); return Json(new { success = false, message = "Error analyzing response. Please try again." }); } } #endregion #region Batch Analysis /// /// Process batch analysis for all responses in a questionnaire /// [HasPermission(Permissions.SurveyAnalysis.Analyze)] public async Task BatchAnalyze(int id) { try { var questionnaire = await _context.Questionnaires .FirstOrDefaultAsync(q => q.Id == id); if (questionnaire == null) { TempData["ErrorMessage"] = "Questionnaire not found."; return RedirectToAction(nameof(Index)); } // Get all responses with ALL answer types var responses = await _context.Responses .Include(r => r.ResponseDetails) .ThenInclude(rd => rd.Question) .Include(r => r.ResponseDetails) .ThenInclude(rd => rd.ResponseAnswers) .ThenInclude(ra => ra.Answer) .Where(r => r.QuestionnaireId == id) .ToListAsync(); var analysisRequests = new List(); foreach (var response in responses) { foreach (var detail in response.ResponseDetails) { // Build combined text from text response + selected answer texts var analysisText = BuildResponseText(detail); if (!string.IsNullOrWhiteSpace(analysisText)) { analysisRequests.Add(new AnalysisRequest { ResponseId = response.Id, QuestionId = detail.QuestionId, ResponseText = analysisText, QuestionText = detail.Question?.Text ?? "" }); } } } if (!analysisRequests.Any()) { TempData["WarningMessage"] = "No analyzable responses found. Responses must contain text or checkbox selections."; return RedirectToAction(nameof(AnalyzeQuestionnaire), new { id }); } ViewBag.QuestionnaireName = questionnaire.Title; ViewBag.QuestionnaireId = id; ViewBag.TotalRequests = analysisRequests.Count; return View("BatchAnalysisProgress"); } catch (Exception ex) { _logger.LogError(ex, "Error starting batch analysis for questionnaire {QuestionnaireId}", id); TempData["ErrorMessage"] = "Error starting batch analysis. Please try again."; return RedirectToAction(nameof(Index)); } } /// /// AJAX endpoint for batch analysis progress /// [HasPermission(Permissions.SurveyAnalysis.Analyze)] [HttpPost] public async Task ProcessBatchAnalysis(int questionnaireId) { try { var responses = await _context.Responses .Include(r => r.ResponseDetails) .ThenInclude(rd => rd.Question) .Include(r => r.ResponseDetails) .ThenInclude(rd => rd.ResponseAnswers) .ThenInclude(ra => ra.Answer) .Where(r => r.QuestionnaireId == questionnaireId) .ToListAsync(); var analysisRequests = new List(); foreach (var response in responses) { foreach (var detail in response.ResponseDetails) { var analysisText = BuildResponseText(detail); if (!string.IsNullOrWhiteSpace(analysisText)) { analysisRequests.Add(new AnalysisRequest { ResponseId = response.Id, QuestionId = detail.QuestionId, ResponseText = analysisText, QuestionText = detail.Question?.Text ?? "" }); } } } var results = await _aiAnalysisService.BatchAnalyzeResponsesAsync(analysisRequests); return Json(new { success = true, processedCount = results.Count, highRiskCount = results.Count(r => r.RiskAssessment?.RiskLevel >= RiskLevel.High), message = $"Successfully analyzed {results.Count} responses." }); } catch (Exception ex) { _logger.LogError(ex, "Error processing batch analysis for questionnaire {QuestionnaireId}", questionnaireId); return Json(new { success = false, message = "Error processing batch analysis. Please try again." }); } } #endregion #region Reporting /// /// Generate detailed analysis report for management /// [HasPermission(Permissions.SurveyAnalysis.Reports)] public async Task GenerateReport(int id) { try { var questionnaire = await _context.Questionnaires .FirstOrDefaultAsync(q => q.Id == id); if (questionnaire == null) { TempData["ErrorMessage"] = "Questionnaire not found."; return RedirectToAction(nameof(Index)); } var report = await _aiAnalysisService.GenerateDetailedAnalysisReportAsync(id); ViewBag.QuestionnaireName = questionnaire.Title; ViewBag.QuestionnaireId = id; ViewBag.Report = report; ViewBag.GeneratedDate = DateTime.Now; return View(); } catch (Exception ex) { _logger.LogError(ex, "Error generating report for questionnaire {QuestionnaireId}", id); TempData["ErrorMessage"] = "Error generating report. Please try again."; return RedirectToAction(nameof(Index)); } } /// /// Download report as text file /// [HasPermission(Permissions.SurveyAnalysis.Reports)] public async Task DownloadReport(int id) { try { var questionnaire = await _context.Questionnaires .FirstOrDefaultAsync(q => q.Id == id); if (questionnaire == null) { TempData["ErrorMessage"] = "Questionnaire not found."; return RedirectToAction(nameof(Index)); } var report = await _aiAnalysisService.GenerateDetailedAnalysisReportAsync(id); var bytes = Encoding.UTF8.GetBytes(report); var fileName = $"Mental_Health_Analysis_{questionnaire.Title}_{DateTime.Now:yyyy-MM-dd}.txt"; return File(bytes, "text/plain", fileName); } catch (Exception ex) { _logger.LogError(ex, "Error downloading report for questionnaire {QuestionnaireId}", id); TempData["ErrorMessage"] = "Error downloading report. Please try again."; return RedirectToAction(nameof(Index)); } } /// /// Export anonymized analysis data /// public async Task ExportAnalysis(int id) { try { var questionnaire = await _context.Questionnaires .FirstOrDefaultAsync(q => q.Id == id); if (questionnaire == null) { TempData["ErrorMessage"] = "Questionnaire not found."; return RedirectToAction(nameof(Index)); } var analysisData = await _aiAnalysisService.ExportAnonymizedAnalysisAsync(id); var json = JsonSerializer.Serialize(analysisData, new JsonSerializerOptions { WriteIndented = true }); var bytes = Encoding.UTF8.GetBytes(json); var fileName = $"Anonymized_Analysis_{questionnaire.Title}_{DateTime.Now:yyyy-MM-dd}.json"; return File(bytes, "application/json", fileName); } catch (Exception ex) { _logger.LogError(ex, "Error exporting analysis for questionnaire {QuestionnaireId}", id); TempData["ErrorMessage"] = "Error exporting analysis. Please try again."; return RedirectToAction(nameof(Index)); } } #endregion #region Mental Health Trends /// /// Analyze mental health trends over time periods /// [HasPermission(Permissions.SurveyAnalysis.Analyze)] public async Task AnalyzeTrends(int id, DateTime? fromDate = null, DateTime? toDate = null) { try { var questionnaire = await _context.Questionnaires .FirstOrDefaultAsync(q => q.Id == id); if (questionnaire == null) { TempData["ErrorMessage"] = "Questionnaire not found."; return RedirectToAction(nameof(Index)); } var from = fromDate ?? DateTime.Now.AddMonths(-6); var to = toDate ?? DateTime.Now; var trends = await _aiAnalysisService.AnalyzeMentalHealthTrendsAsync(id, from, to); ViewBag.QuestionnaireName = questionnaire.Title; ViewBag.QuestionnaireId = id; ViewBag.FromDate = from; ViewBag.ToDate = to; return View(trends); } catch (Exception ex) { _logger.LogError(ex, "Error analyzing trends for questionnaire {QuestionnaireId}", id); TempData["ErrorMessage"] = "Error analyzing trends. Please try again."; return RedirectToAction(nameof(Index)); } } #endregion #region Service Health and Testing /// /// Check Claude AI service health status (AJAX) /// public async Task ServiceHealth() { try { var healthStatus = await _aiAnalysisService.GetServiceHealthStatusAsync(); return Json(new { success = true, services = healthStatus, message = "Service health check completed successfully" }); } catch (Exception ex) { _logger.LogError(ex, "Error checking service health"); return Json(new { success = false, error = "Unable to check service health", message = ex.Message }); } } /// /// Test Claude AI analysis with sample text (AJAX) /// [HttpPost] public async Task TestAnalysis(string sampleText) { try { if (string.IsNullOrWhiteSpace(sampleText)) return Json(new { success = false, message = "Please provide sample text." }); var analysisRequest = new AnalysisRequest { ResponseId = 0, QuestionId = 0, ResponseText = sampleText, QuestionText = "Test question: How are you feeling about your work environment?" }; var analysis = await _aiAnalysisService.AnalyzeCompleteResponseAsync(analysisRequest); return Json(new { success = true, sentiment = analysis.SentimentAnalysis?.Sentiment, riskLevel = analysis.RiskAssessment?.RiskLevel.ToString(), keyPhrases = analysis.KeyPhrases?.KeyPhrases, insights = analysis.Insights.Select(i => i.Category).ToList() }); } catch (Exception ex) { _logger.LogError(ex, "Error in test analysis"); return Json(new { success = false, message = "Error performing test analysis. Please try again." }); } } #endregion #region Management Dashboard /// /// Executive dashboard for mental health overview /// public async Task Dashboard(int id) { try { var questionnaire = await _context.Questionnaires .FirstOrDefaultAsync(q => q.Id == id); if (questionnaire == null) { TempData["ErrorMessage"] = "Questionnaire not found."; return RedirectToAction(nameof(Index)); } var dashboard = await _aiAnalysisService.GenerateManagementDashboardAsync(id); return View(dashboard); } catch (Exception ex) { _logger.LogError(ex, "Error loading dashboard for questionnaire {QuestionnaireId}", id); TempData["ErrorMessage"] = "Error loading dashboard. Please try again."; return RedirectToAction(nameof(Index)); } } /// /// Builds analysis text from a ResponseDetail, combining text responses and selected answer texts. /// private string BuildResponseText(ResponseDetail detail) { var parts = new List(); if (!string.IsNullOrWhiteSpace(detail.TextResponse)) { parts.Add(detail.TextResponse); } if (detail.ResponseAnswers != null && detail.ResponseAnswers.Any()) { foreach (var ra in detail.ResponseAnswers) { if (ra.Answer != null && !string.IsNullOrWhiteSpace(ra.Answer.Text)) { parts.Add(ra.Answer.Text); } } } return string.Join(". ", parts); } #region Case Management — Notes, Status, Action Plans, PDF Export, History // ───────────────────────────────────────────── // CASE NOTES CRUD // ───────────────────────────────────────────── /// /// Get all notes for a response (AJAX) /// [HttpGet] public async Task GetCaseNotes(int responseId) { var notes = await _context.CaseNotes .Where(n => n.ResponseId == responseId) .OrderByDescending(n => n.CreatedAt) .Select(n => new { n.Id, n.AuthorName, n.AuthorEmail, n.NoteText, n.Category, n.IsConfidential, CreatedAt = n.CreatedAt.ToString("MMM dd, yyyy HH:mm"), UpdatedAt = n.UpdatedAt.HasValue ? n.UpdatedAt.Value.ToString("MMM dd, yyyy HH:mm") : null }) .ToListAsync(); return Json(new { success = true, notes }); } /// /// Add a case note (AJAX) /// [HttpPost] public async Task AddCaseNote(int responseId, string noteText, string category) { try { if (string.IsNullOrWhiteSpace(noteText)) return Json(new { success = false, message = "Note text is required." }); var currentUser = User.Identity?.Name ?? "Unknown"; var currentEmail = User.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Email)?.Value; var note = new CaseNote { ResponseId = responseId, AuthorName = currentUser, AuthorEmail = currentEmail, NoteText = noteText.Trim(), Category = string.IsNullOrWhiteSpace(category) ? "General" : category, CreatedAt = DateTime.UtcNow }; _context.CaseNotes.Add(note); await _context.SaveChangesAsync(); return Json(new { success = true, note = new { note.Id, note.AuthorName, note.AuthorEmail, note.NoteText, note.Category, note.IsConfidential, CreatedAt = note.CreatedAt.ToString("MMM dd, yyyy HH:mm") } }); } catch (Exception ex) { _logger.LogError(ex, "Error adding case note for ResponseId {ResponseId}", responseId); return Json(new { success = false, message = "Failed to save note." }); } } /// /// Delete a case note (AJAX) /// [HttpPost] public async Task DeleteCaseNote(int noteId) { try { var note = await _context.CaseNotes.FindAsync(noteId); if (note == null) return Json(new { success = false, message = "Note not found." }); _context.CaseNotes.Remove(note); await _context.SaveChangesAsync(); return Json(new { success = true }); } catch (Exception ex) { _logger.LogError(ex, "Error deleting case note {NoteId}", noteId); return Json(new { success = false, message = "Failed to delete note." }); } } // ───────────────────────────────────────────── // CASE STATUS TRACKING // ───────────────────────────────────────────── /// /// Get status history for a response (AJAX) /// [HttpGet] public async Task GetCaseStatus(int responseId) { var history = await _context.CaseStatusEntries .Where(s => s.ResponseId == responseId) .OrderByDescending(s => s.ChangedAt) .Select(s => new { s.Id, Status = s.Status.ToString(), StatusInt = (int)s.Status, s.ChangedByName, s.Reason, ChangedAt = s.ChangedAt.ToString("MMM dd, yyyy HH:mm") }) .ToListAsync(); // Current status = most recent entry, or "New" if no entries var currentStatus = history.FirstOrDefault()?.Status ?? "New"; var currentStatusInt = history.FirstOrDefault()?.StatusInt ?? 0; return Json(new { success = true, currentStatus, currentStatusInt, history }); } /// /// Update case status (AJAX) /// [HttpPost] public async Task UpdateCaseStatus(int responseId, int newStatus, string reason) { try { if (!Enum.IsDefined(typeof(CaseStatusType), newStatus)) return Json(new { success = false, message = "Invalid status." }); var currentUser = User.Identity?.Name ?? "Unknown"; var currentEmail = User.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Email)?.Value; var entry = new CaseStatusEntry { ResponseId = responseId, Status = (CaseStatusType)newStatus, ChangedByName = currentUser, ChangedByEmail = currentEmail, Reason = string.IsNullOrWhiteSpace(reason) ? null : reason.Trim(), ChangedAt = DateTime.UtcNow }; _context.CaseStatusEntries.Add(entry); await _context.SaveChangesAsync(); return Json(new { success = true, entry = new { entry.Id, Status = entry.Status.ToString(), StatusInt = (int)entry.Status, entry.ChangedByName, entry.Reason, ChangedAt = entry.ChangedAt.ToString("MMM dd, yyyy HH:mm") } }); } catch (Exception ex) { _logger.LogError(ex, "Error updating case status for ResponseId {ResponseId}", responseId); return Json(new { success = false, message = "Failed to update status." }); } } // ───────────────────────────────────────────── // ACTION PLANS CRUD // ───────────────────────────────────────────── /// /// Get all action plans for a response (AJAX) /// [HttpGet] public async Task GetActionPlans(int responseId) { var plans = await _context.ActionPlans .Where(a => a.ResponseId == responseId) .OrderByDescending(a => a.CreatedAt) .Select(a => new { a.Id, a.Title, a.Description, a.ActionType, a.Priority, a.Status, a.AssignedTo, a.AssignedToEmail, ScheduledDate = a.ScheduledDate.HasValue ? a.ScheduledDate.Value.ToString("MMM dd, yyyy HH:mm") : null, CompletedDate = a.CompletedDate.HasValue ? a.CompletedDate.Value.ToString("MMM dd, yyyy HH:mm") : null, a.CompletionNotes, a.CreatedByName, CreatedAt = a.CreatedAt.ToString("MMM dd, yyyy HH:mm") }) .ToListAsync(); return Json(new { success = true, plans }); } /// /// Create a new action plan (AJAX) /// [HttpPost] public async Task CreateActionPlan( int responseId, string title, string description, string actionType, string priority, string assignedTo, string assignedToEmail, string scheduledDate) { try { if (string.IsNullOrWhiteSpace(title)) return Json(new { success = false, message = "Title is required." }); var currentUser = User.Identity?.Name ?? "Unknown"; var currentEmail = User.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Email)?.Value; var plan = new ActionPlan { ResponseId = responseId, Title = title.Trim(), Description = description?.Trim(), ActionType = string.IsNullOrWhiteSpace(actionType) ? "ImmediateContact" : actionType, Priority = string.IsNullOrWhiteSpace(priority) ? "Normal" : priority, Status = "Pending", AssignedTo = assignedTo?.Trim(), AssignedToEmail = assignedToEmail?.Trim(), ScheduledDate = DateTime.TryParse(scheduledDate, out var sd) ? sd : null, CreatedByName = currentUser, CreatedByEmail = currentEmail, CreatedAt = DateTime.UtcNow }; _context.ActionPlans.Add(plan); await _context.SaveChangesAsync(); // Also auto-add a status entry for "InterventionScheduled" if not already var hasIntervention = await _context.CaseStatusEntries .AnyAsync(s => s.ResponseId == responseId && s.Status == CaseStatusType.InterventionScheduled); if (!hasIntervention) { _context.CaseStatusEntries.Add(new CaseStatusEntry { ResponseId = responseId, Status = CaseStatusType.InterventionScheduled, ChangedByName = currentUser, ChangedByEmail = currentEmail, Reason = $"Action plan created: {title}", ChangedAt = DateTime.UtcNow }); await _context.SaveChangesAsync(); } return Json(new { success = true, plan = new { plan.Id, plan.Title, plan.Description, plan.ActionType, plan.Priority, plan.Status, plan.AssignedTo, ScheduledDate = plan.ScheduledDate?.ToString("MMM dd, yyyy HH:mm"), plan.CreatedByName, CreatedAt = plan.CreatedAt.ToString("MMM dd, yyyy HH:mm") } }); } catch (Exception ex) { _logger.LogError(ex, "Error creating action plan for ResponseId {ResponseId}", responseId); return Json(new { success = false, message = "Failed to create action plan." }); } } /// /// Update action plan status (AJAX) — mark as InProgress, Completed, Cancelled /// [HttpPost] public async Task UpdateActionPlanStatus(int planId, string status, string completionNotes) { try { var plan = await _context.ActionPlans.FindAsync(planId); if (plan == null) return Json(new { success = false, message = "Action plan not found." }); plan.Status = status; plan.UpdatedAt = DateTime.UtcNow; if (status == "Completed") { plan.CompletedDate = DateTime.UtcNow; plan.CompletionNotes = completionNotes?.Trim(); } await _context.SaveChangesAsync(); return Json(new { success = true, newStatus = plan.Status }); } catch (Exception ex) { _logger.LogError(ex, "Error updating action plan {PlanId}", planId); return Json(new { success = false, message = "Failed to update action plan." }); } } // ───────────────────────────────────────────── // RESPONDENT HISTORY TIMELINE // ───────────────────────────────────────────── /// /// Get all responses from the same user across all questionnaires (AJAX) /// [HttpGet] public async Task GetRespondentHistory(int responseId) { try { // Get the current response to find the user var currentResponse = await _context.Responses .FirstOrDefaultAsync(r => r.Id == responseId); if (currentResponse == null) return Json(new { success = false, message = "Response not found." }); var userEmail = currentResponse.UserEmail; var userName = currentResponse.UserName; // Find all responses from this user var history = await _context.Responses .Include(r => r.Questionnaire) .Include(r => r.ResponseDetails) .ThenInclude(rd => rd.ResponseAnswers) .Where(r => (!string.IsNullOrEmpty(userEmail) && r.UserEmail == userEmail) || (!string.IsNullOrEmpty(userName) && r.UserName == userName)) .OrderByDescending(r => r.SubmissionDate) .Select(r => new { r.Id, QuestionnaireTitle = r.Questionnaire != null ? r.Questionnaire.Title : "Unknown", r.QuestionnaireId, SubmissionDate = r.SubmissionDate.ToString("MMM dd, yyyy HH:mm"), SubmissionDateRaw = r.SubmissionDate, TotalAnswered = r.ResponseDetails.Count, TextResponses = r.ResponseDetails.Count(rd => !string.IsNullOrEmpty(rd.TextResponse)), CheckboxResponses = r.ResponseDetails.Count(rd => rd.ResponseAnswers.Any()), IsCurrent = r.Id == responseId, // Get case status if any LatestStatus = _context.CaseStatusEntries .Where(s => s.ResponseId == r.Id) .OrderByDescending(s => s.ChangedAt) .Select(s => s.Status.ToString()) .FirstOrDefault() }) .ToListAsync(); return Json(new { success = true, userName = userName ?? "Anonymous", userEmail = userEmail ?? "No email", totalResponses = history.Count, history }); } catch (Exception ex) { _logger.LogError(ex, "Error getting respondent history for ResponseId {ResponseId}", responseId); return Json(new { success = false, message = "Failed to load history." }); } } // ───────────────────────────────────────────── // PDF EXPORT — Case Report // ───────────────────────────────────────────── /// /// Export case as PDF report (download) /// [HttpGet] public async Task ExportCasePdf(int questionnaireId, int responseId) { try { var response = await _context.Responses .Include(r => r.Questionnaire) .Include(r => r.ResponseDetails) .ThenInclude(rd => rd.Question) .Include(r => r.ResponseDetails) .ThenInclude(rd => rd.ResponseAnswers) .ThenInclude(ra => ra.Answer) .FirstOrDefaultAsync(r => r.Id == responseId && r.QuestionnaireId == questionnaireId); if (response == null) return NotFound(); // Get analysis results var analysisResults = new List(); foreach (var detail in response.ResponseDetails) { var analysisText = BuildResponseText(detail); if (!string.IsNullOrWhiteSpace(analysisText)) { var analysisRequest = new AnalysisRequest { ResponseId = response.Id, QuestionId = detail.QuestionId, ResponseText = analysisText, QuestionText = detail.Question?.Text ?? "" }; var analysis = await _aiAnalysisService.AnalyzeCompleteResponseAsync(analysisRequest); analysisResults.Add(analysis); } } // Get case notes var notes = await _context.CaseNotes .Where(n => n.ResponseId == responseId) .OrderByDescending(n => n.CreatedAt) .ToListAsync(); // Get status history var statusHistory = await _context.CaseStatusEntries .Where(s => s.ResponseId == responseId) .OrderByDescending(s => s.ChangedAt) .ToListAsync(); // Get action plans var actionPlans = await _context.ActionPlans .Where(a => a.ResponseId == responseId) .OrderByDescending(a => a.CreatedAt) .ToListAsync(); // Build PDF content as structured text (to be rendered by view) var reportData = new { Response = response, Analysis = analysisResults, Notes = notes, StatusHistory = statusHistory, ActionPlans = actionPlans, GeneratedAt = DateTime.UtcNow, GeneratedBy = User.Identity?.Name ?? "System" }; // Build plain text report for download var report = new System.Text.StringBuilder(); report.AppendLine("═══════════════════════════════════════════════════"); report.AppendLine(" NVKN — MENTAL HEALTH CASE REPORT (CONFIDENTIAL)"); report.AppendLine("═══════════════════════════════════════════════════"); report.AppendLine(); report.AppendLine($"Generated: {DateTime.UtcNow:yyyy-MM-dd HH:mm} UTC"); report.AppendLine($"Generated By: {User.Identity?.Name ?? "System"}"); report.AppendLine($"Response ID: #{response.Id}"); report.AppendLine($"Questionnaire: {response.Questionnaire?.Title}"); report.AppendLine($"Respondent: {response.UserName ?? "Anonymous"}"); report.AppendLine($"Submission Date: {response.SubmissionDate:yyyy-MM-dd HH:mm}"); report.AppendLine(); // Status report.AppendLine("── CASE STATUS ──────────────────────────────────"); if (statusHistory.Any()) { var current = statusHistory.First(); report.AppendLine($"Current Status: {current.Status}"); report.AppendLine($"Last Updated: {current.ChangedAt:yyyy-MM-dd HH:mm} by {current.ChangedByName}"); if (!string.IsNullOrEmpty(current.Reason)) report.AppendLine($"Reason: {current.Reason}"); } else { report.AppendLine("Current Status: New"); } report.AppendLine(); // Analysis Results report.AppendLine("── AI ANALYSIS RESULTS ─────────────────────────"); foreach (var analysis in analysisResults) { report.AppendLine(); report.AppendLine($"Question: {analysis.QuestionText}"); report.AppendLine($"Response (Anonymized): {analysis.AnonymizedResponseText}"); if (analysis.SentimentAnalysis != null) { report.AppendLine($"Sentiment: {analysis.SentimentAnalysis.Sentiment}"); report.AppendLine($" Positive: {Math.Round(analysis.SentimentAnalysis.PositiveScore * 100, 1)}%"); report.AppendLine($" Neutral: {Math.Round(analysis.SentimentAnalysis.NeutralScore * 100, 1)}%"); report.AppendLine($" Negative: {Math.Round(analysis.SentimentAnalysis.NegativeScore * 100, 1)}%"); } if (analysis.RiskAssessment != null) { report.AppendLine($"Risk Level: {analysis.RiskAssessment.RiskLevel}"); report.AppendLine($"Risk Score: {Math.Round(analysis.RiskAssessment.RiskScore * 100, 0)}%"); if (analysis.RiskAssessment.RequiresImmediateAttention) report.AppendLine("⚠ REQUIRES IMMEDIATE ATTENTION"); if (!string.IsNullOrEmpty(analysis.RiskAssessment.RecommendedAction)) report.AppendLine($"Recommended Action: {analysis.RiskAssessment.RecommendedAction}"); if (analysis.RiskAssessment.RiskIndicators?.Any() == true) report.AppendLine($"Risk Indicators: {string.Join(", ", analysis.RiskAssessment.RiskIndicators)}"); if (analysis.RiskAssessment.ProtectiveFactors?.Any() == true) report.AppendLine($"Protective Factors: {string.Join(", ", analysis.RiskAssessment.ProtectiveFactors)}"); } if (analysis.KeyPhrases?.KeyPhrases?.Any() == true) report.AppendLine($"Key Phrases: {string.Join(", ", analysis.KeyPhrases.KeyPhrases)}"); if (analysis.Insights?.Any() == true) { report.AppendLine("Workplace Insights:"); foreach (var insight in analysis.Insights) { report.AppendLine($" [{insight.Category}] {insight.Issue}"); report.AppendLine($" → {insight.RecommendedIntervention} (Priority: {insight.Priority})"); } } report.AppendLine(" ─ ─ ─ ─ ─ ─ ─ ─ ─ ─"); } report.AppendLine(); // Action Plans report.AppendLine("── ACTION PLANS ────────────────────────────────"); if (actionPlans.Any()) { foreach (var plan in actionPlans) { report.AppendLine($" [{plan.Priority}] {plan.Title} — {plan.Status}"); report.AppendLine($" Type: {plan.ActionType}"); if (!string.IsNullOrEmpty(plan.AssignedTo)) report.AppendLine($" Assigned To: {plan.AssignedTo}"); if (plan.ScheduledDate.HasValue) report.AppendLine($" Scheduled: {plan.ScheduledDate.Value:yyyy-MM-dd HH:mm}"); if (!string.IsNullOrEmpty(plan.Description)) report.AppendLine($" Description: {plan.Description}"); if (plan.CompletedDate.HasValue) report.AppendLine($" Completed: {plan.CompletedDate.Value:yyyy-MM-dd HH:mm}"); report.AppendLine(); } } else { report.AppendLine(" No action plans created yet."); } report.AppendLine(); // Case Notes report.AppendLine("── CASE NOTES ──────────────────────────────────"); if (notes.Any()) { foreach (var note in notes) { report.AppendLine($" [{note.Category}] {note.CreatedAt:yyyy-MM-dd HH:mm} — {note.AuthorName}"); report.AppendLine($" {note.NoteText}"); report.AppendLine(); } } else { report.AppendLine(" No case notes recorded yet."); } report.AppendLine(); // Status History report.AppendLine("── STATUS CHANGE LOG ───────────────────────────"); if (statusHistory.Any()) { foreach (var entry in statusHistory) { report.AppendLine($" {entry.ChangedAt:yyyy-MM-dd HH:mm} — {entry.Status} by {entry.ChangedByName}"); if (!string.IsNullOrEmpty(entry.Reason)) report.AppendLine($" Reason: {entry.Reason}"); } } else { report.AppendLine(" No status changes recorded."); } report.AppendLine(); report.AppendLine("═══════════════════════════════════════════════════"); report.AppendLine(" END OF REPORT — CONFIDENTIAL"); report.AppendLine("═══════════════════════════════════════════════════"); var bytes = System.Text.Encoding.UTF8.GetBytes(report.ToString()); var fileName = $"CaseReport_Response{responseId}_{DateTime.UtcNow:yyyyMMdd_HHmm}.txt"; return File(bytes, "text/plain", fileName); } catch (Exception ex) { _logger.LogError(ex, "Error exporting case PDF for ResponseId {ResponseId}", responseId); TempData["ErrorMessage"] = "Error exporting case report."; return RedirectToAction(nameof(ViewHighRiskResponse), new { questionnaireId, responseId }); } } #endregion #endregion } }