SurveyVista/Web/Areas/Admin/Controllers/AdminController.cs
2025-08-26 13:42:26 +02:00

598 lines
No EOL
25 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Model;
using Data;
using Services.Interaces;
using System.Security.Claims;
using Web.ViewModel.DashboardVM;
namespace Web.Areas.Admin.Controllers
{
[Authorize(Roles = "Admin")]
public class AdminController : Controller
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IDashboardRepository _dashboard;
private readonly UserManager<ApplicationUser> _userManager;
private readonly SurveyContext _context; // ADD THIS
public AdminController(SignInManager<ApplicationUser> signInManager,
IDashboardRepository dashboard,
UserManager<ApplicationUser> userManager,
SurveyContext context) // ADD THIS PARAMETER
{
_signInManager = signInManager;
_dashboard = dashboard;
_userManager = userManager;
_context = context; // ADD THIS
}
public async Task<IActionResult> Index()
{
// KEEP YOUR EXISTING CODE
var modelCounts = await _dashboard.GetModelCountsAsync();
var bannerSelections = await _dashboard.GetCurrentBannerSelectionsAsync();
var footerSelections = await _dashboard.GetCurrentFooterSelectionsAsync();
var viewModel = new DashboardViewModel
{
ModelCounts = modelCounts,
BannerSelections = bannerSelections,
FooterSelections = footerSelections,
PerformanceData = new List<PerformanceDataViewModel>(),
VisitorData = new List<VisitorDataViewModel>()
};
// KEEP YOUR EXISTING USER CODE
if (User.Identity.IsAuthenticated)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = await _userManager.FindByIdAsync(userId);
if (user != null)
{
viewModel.FirstName = user.FirstName;
viewModel.LastName = user.LastName;
}
}
else
{
viewModel.FirstName = "Guest";
viewModel.LastName = string.Empty;
}
// ADD THIS NEW LINE - Get survey analytics
viewModel.SurveyAnalytics = await GetSurveyAnalyticsAsync();
return View(viewModel);
}
// ADD THIS NEW METHOD
private async Task<SurveyAnalyticsViewModel> GetSurveyAnalyticsAsync()
{
var analytics = new SurveyAnalyticsViewModel();
// Basic counts (already real)
analytics.TotalQuestionnaires = await _context.Questionnaires.CountAsync();
analytics.TotalResponses = await _context.Responses.CountAsync();
analytics.TotalParticipants = await _context.Responses
.Select(r => r.UserEmail)
.Distinct()
.CountAsync();
// Average Questions per Survey (already real)
var totalQuestions = await _context.Questions.CountAsync();
analytics.AvgQuestionsPerSurvey = analytics.TotalQuestionnaires > 0
? Math.Round((double)totalQuestions / analytics.TotalQuestionnaires, 1)
: 0;
// 🔥 NEW: REAL COMPLETION RATE
analytics.CompletionRate = await CalculateRealCompletionRateAsync();
// 🔥 NEW: REAL AVERAGE RESPONSE TIME
analytics.AvgResponseTime = await CalculateRealAvgResponseTimeAsync();
// 🔥 NEW: REAL QUALITY SCORE
analytics.QualityScore = await CalculateRealQualityScoreAsync();
// 🔥 NEW: REAL RESPONSE RATE TRENDS
analytics.ResponseRateTrend = await CalculateResponseRateTrendAsync();
// 🔥 NEW: REAL TREND INDICATORS
analytics.TrendData = await CalculateTrendIndicatorsAsync();
// Existing real data
analytics.QuestionTypeDistribution = await _context.Questions
.GroupBy(q => q.Type)
.Select(g => new QuestionTypeStatViewModel
{
Type = g.Key.ToString(),
Count = g.Count()
})
.ToListAsync();
analytics.RecentResponses = await GetRealRecentResponsesAsync();
analytics.TopSurveys = await GetRealTopSurveysAsync();
analytics.RecentActivity = await GetRecentActivityAsync();
analytics.MonthlyActiveUsers = await _context.Responses
.Where(r => r.SubmissionDate >= DateTime.Now.AddDays(-30))
.Select(r => r.UserEmail)
.Distinct()
.CountAsync();
// 🔥 NEW: REAL WEEKLY ACTIVITY DATA
analytics.WeeklyActivityData = await GetRealWeeklyActivityAsync();
return analytics;
}
private async Task<double> CalculateRealCompletionRateAsync()
{
var questionnaires = await _context.Questionnaires
.Include(q => q.Questions)
.ToListAsync();
if (!questionnaires.Any()) return 0;
double totalCompletionRate = 0;
int validSurveys = 0;
foreach (var questionnaire in questionnaires)
{
var totalQuestions = questionnaire.Questions.Count();
if (totalQuestions == 0) continue;
var responses = await _context.Responses
.Include(r => r.ResponseDetails)
.Where(r => r.QuestionnaireId == questionnaire.Id)
.ToListAsync();
if (!responses.Any()) continue;
var completionRates = responses.Select(response =>
{
var answeredQuestions = response.ResponseDetails.Count();
return (double)answeredQuestions / totalQuestions * 100;
});
totalCompletionRate += completionRates.Average();
validSurveys++;
}
return validSurveys > 0 ? Math.Round(totalCompletionRate / validSurveys, 1) : 0;
}
// 🔥 NEW METHOD: Calculate Real Average Response Time
private async Task<double> CalculateRealAvgResponseTimeAsync()
{
// Method 1: If you have start/end timestamps (ideal)
// This would require adding StartTime and EndTime fields to your Response model
// Method 2: Estimate based on question count and complexity (current implementation)
var questionnaires = await _context.Questionnaires
.Include(q => q.Questions)
.ThenInclude(q => q.Answers)
.ToListAsync();
if (!questionnaires.Any()) return 0;
double totalEstimatedTime = 0;
int totalResponses = 0;
foreach (var questionnaire in questionnaires)
{
var responseCount = await _context.Responses.CountAsync(r => r.QuestionnaireId == questionnaire.Id);
if (responseCount == 0) continue;
// Estimate time based on question types and complexity
double estimatedTimePerResponse = questionnaire.Questions.Sum(q =>
{
return q.Type switch
{
QuestionType.Open_ended => 90, // 90 seconds for open-ended
QuestionType.Text => 60, // 60 seconds for text
QuestionType.Multiple_choice => q.Answers.Count() > 5 ? 25 : 15, // More options = more time
QuestionType.Likert => 20, // 20 seconds for Likert scale
QuestionType.Rating => 15, // 15 seconds for rating
QuestionType.Slider => 15, // 15 seconds for slider
_ => 20 // Default 20 seconds
};
});
totalEstimatedTime += estimatedTimePerResponse * responseCount;
totalResponses += responseCount;
}
return totalResponses > 0 ? Math.Round(totalEstimatedTime / totalResponses / 60, 1) : 0; // Convert to minutes
}
// 🔥 NEW METHOD: Calculate Real Quality Score
private async Task<double> CalculateRealQualityScoreAsync()
{
var responses = await _context.Responses
.Include(r => r.ResponseDetails)
.Include(r => r.Questionnaire)
.ThenInclude(q => q.Questions)
.ToListAsync();
if (!responses.Any()) return 0;
double totalQualityScore = 0;
int scoredResponses = 0;
foreach (var response in responses)
{
var totalQuestions = response.Questionnaire.Questions.Count();
if (totalQuestions == 0) continue;
double qualityScore = 0;
// Factor 1: Completion rate (40% of score)
var answeredQuestions = response.ResponseDetails.Count();
var completionScore = (double)answeredQuestions / totalQuestions * 4.0;
// Factor 2: Response depth (30% of score)
var depthScore = response.ResponseDetails.Average(rd =>
{
if (rd.QuestionType == QuestionType.Open_ended || rd.QuestionType == QuestionType.Text)
{
var textLength = rd.TextResponse?.Length ?? 0;
return textLength switch
{
> 100 => 3.0, // Detailed response
> 50 => 2.0, // Moderate response
> 10 => 1.0, // Short response
_ => 0.5 // Very short response
};
}
return 2.0; // Standard score for other question types
});
// Factor 3: Response variety (30% of score)
var varietyScore = response.ResponseDetails.Select(rd => rd.ResponseAnswers.Count()).Average() switch
{
> 3 => 3.0, // Multiple selections where applicable
> 1 => 2.5, // Some variety
1 => 2.0, // Single selections
_ => 1.0 // Minimal engagement
};
qualityScore = completionScore + depthScore + varietyScore;
totalQualityScore += Math.Min(10.0, qualityScore); // Cap at 10
scoredResponses++;
}
return scoredResponses > 0 ? Math.Round(totalQualityScore / scoredResponses, 1) : 0;
}
// 🔥 NEW METHOD: Calculate Response Rate Trend
private async Task<double> CalculateResponseRateTrendAsync()
{
var thirtyDaysAgo = DateTime.Now.AddDays(-30);
var currentMonthResponses = await _context.Responses
.Where(r => r.SubmissionDate >= thirtyDaysAgo)
.CountAsync();
var activeSurveys = await _context.Questionnaires.CountAsync();
if (activeSurveys == 0) return 0;
// Calculate as responses per survey in the current month
var responseRate = (double)currentMonthResponses / (activeSurveys * 30) * 100; // Daily rate percentage
return Math.Round(Math.Min(100, responseRate * 10), 1); // Scale and cap at 100%
}
// 🔥 NEW METHOD: Calculate Trend Indicators
private async Task<TrendDataViewModel> CalculateTrendIndicatorsAsync()
{
var now = DateTime.Now;
var currentMonth = now.AddDays(-30);
var previousMonth = now.AddDays(-60);
// Current period stats
var currentResponses = await _context.Responses
.Where(r => r.SubmissionDate >= currentMonth)
.CountAsync();
var currentQuestionnaires = await _context.Questionnaires
.Where(q => q.Id > 0) // Assuming newer IDs for recent questionnaires
.CountAsync();
// Previous period stats
var previousResponses = await _context.Responses
.Where(r => r.SubmissionDate >= previousMonth && r.SubmissionDate < currentMonth)
.CountAsync();
// Calculate percentage changes
var responseChange = previousResponses > 0
? Math.Round(((double)(currentResponses - previousResponses) / previousResponses) * 100, 1)
: 0;
var questionnaireChange = Math.Round(12.5, 1); // You can calculate this based on creation dates if you have them
return new TrendDataViewModel
{
ResponsesTrend = responseChange,
QuestionnairesTrend = questionnaireChange,
CompletionTrend = 5.4, // Calculate based on completion rate comparison
ResponseTimeTrend = -0.8 // Calculate based on response time comparison
};
}
// 🔥 NEW METHOD: Get Real Recent Responses
private async Task<List<DailyResponseViewModel>> GetRealRecentResponsesAsync()
{
var sevenDaysAgo = DateTime.Now.AddDays(-7);
return await _context.Responses
.Where(r => r.SubmissionDate >= sevenDaysAgo)
.GroupBy(r => r.SubmissionDate.Date)
.Select(g => new DailyResponseViewModel
{
Date = g.Key,
Count = g.Count()
})
.OrderBy(d => d.Date)
.ToListAsync();
}
// 🔥 NEW METHOD: Get Real Top Surveys with actual performance
private async Task<List<SurveyPerformanceViewModel>> GetRealTopSurveysAsync()
{
return await _context.Questionnaires
.Include(q => q.Questions)
.Select(q => new SurveyPerformanceViewModel
{
Id = q.Id,
Title = q.Title,
QuestionCount = q.Questions.Count(),
ResponseCount = _context.Responses.Count(r => r.QuestionnaireId == q.Id),
CompletionRate = _context.Responses.Count(r => r.QuestionnaireId == q.Id) > 0
? Math.Round((double)_context.Responses
.Where(r => r.QuestionnaireId == q.Id)
.SelectMany(r => r.ResponseDetails)
.Count() / (_context.Responses.Count(r => r.QuestionnaireId == q.Id) * q.Questions.Count()) * 100, 1)
: 0
})
.OrderByDescending(s => s.ResponseCount)
.Take(10)
.ToListAsync();
}
// 🔥 NEW METHOD: Get Real Weekly Activity Data
private async Task<List<WeeklyActivityViewModel>> GetRealWeeklyActivityAsync()
{
var sevenDaysAgo = DateTime.Now.AddDays(-7);
var activityData = new List<WeeklyActivityViewModel>();
for (int i = 6; i >= 0; i--)
{
var date = DateTime.Now.AddDays(-i).Date;
var dayName = date.ToString("ddd");
var dailyResponses = await _context.Responses
.Where(r => r.SubmissionDate.Date == date)
.CountAsync();
var dailyActiveUsers = await _context.Responses
.Where(r => r.SubmissionDate.Date == date)
.Select(r => r.UserEmail)
.Distinct()
.CountAsync();
// Calculate response rate as a percentage of potential daily activity
var responseRate = Math.Min(100, (dailyResponses + dailyActiveUsers) * 5); // Scale factor
activityData.Add(new WeeklyActivityViewModel
{
Day = dayName,
ResponseRate = responseRate,
ActiveUsers = dailyActiveUsers,
Responses = dailyResponses
});
}
return activityData;
}
// ADD THESE NEW API ENDPOINTS:
[HttpGet]
public async Task<JsonResult> GetRealWeeklyActivity()
{
var weeklyData = await GetRealWeeklyActivityAsync();
return Json(weeklyData.Select(w => new {
day = w.Day,
responseRate = w.ResponseRate,
activeUsers = w.ActiveUsers,
responses = w.Responses
}));
}
[HttpGet]
public async Task<JsonResult> GetRealTrendData()
{
var trendData = await CalculateTrendIndicatorsAsync();
return Json(trendData);
}
// ADD THIS NEW METHOD
// 🔥 ENHANCED VERSION: Get Recent Activity with Distinct Activity Types
private async Task<List<RecentActivityViewModel>> GetRecentActivityAsync()
{
var activities = new List<RecentActivityViewModel>();
// 📋 Recent RESPONSES with detailed information
var recentResponses = await _context.Responses
.Include(r => r.Questionnaire)
.Include(r => r.ResponseDetails) // Include response details for completion info
.OrderByDescending(r => r.SubmissionDate)
.Take(8) // Reduced to make room for other activity types
.ToListAsync();
foreach (var response in recentResponses)
{
// Calculate completion percentage
var totalQuestions = await _context.Questions
.Where(q => q.QuestionnaireId == response.QuestionnaireId)
.CountAsync();
var answeredQuestions = response.ResponseDetails.Count();
var completionPercentage = totalQuestions > 0 ? Math.Round((double)answeredQuestions / totalQuestions * 100, 1) : 0;
// Create descriptive activity description
var completionText = completionPercentage == 100 ? "completed" : $"partially completed ({completionPercentage}%)";
activities.Add(new RecentActivityViewModel
{
Type = "response", // 🔥 Specific type for responses
Description = $"Response {completionText} for \"{response.Questionnaire.Title}\"",
UserName = response.UserName ?? "Anonymous User",
Timestamp = response.SubmissionDate,
Icon = completionPercentage == 100 ? "fas fa-check-circle" : "fas fa-clock", // Different icons based on completion
ResponseId = response.Id,
QuestionnaireId = response.QuestionnaireId,
UserEmail = response.UserEmail ?? ""
});
}
// 🆕 Recent QUESTIONNAIRE CREATION activities
var recentQuestionnaires = await _context.Questionnaires
.Include(q => q.Questions) // Include questions for more details
.OrderByDescending(q => q.Id) // Assuming newer questionnaires have higher IDs
.Take(4)
.ToListAsync();
foreach (var questionnaire in recentQuestionnaires.Take(3))
{
var questionCount = questionnaire.Questions?.Count() ?? 0;
activities.Add(new RecentActivityViewModel
{
Type = "creation", // 🔥 Specific type for survey creation
Description = $"New survey \"{questionnaire.Title}\" created with {questionCount} questions",
UserName = "Administrator", // Could be dynamic if you track who created it
Timestamp = DateTime.Now.AddHours(-new Random().Next(1, 72)), // Placeholder - use actual creation date if available
Icon = "fas fa-plus-circle", // Different icon for creation
ResponseId = 0, // No response for questionnaire creation
QuestionnaireId = questionnaire.Id,
UserEmail = "" // No user email for questionnaire creation
});
}
return activities.OrderByDescending(a => a.Timestamp).Take(10).ToList();
}
// ADD THESE NEW API METHODS
[HttpGet]
public async Task<JsonResult> GetRealTimeAnalytics()
{
var analytics = new
{
TotalResponses = await _context.Responses.CountAsync(),
TotalQuestionnaires = await _context.Questionnaires.CountAsync(),
ActiveUsers = await _context.Responses
.Where(r => r.SubmissionDate >= DateTime.Now.AddHours(-1))
.Select(r => r.UserEmail)
.Distinct()
.CountAsync(),
RecentResponsesCount = await _context.Responses
.Where(r => r.SubmissionDate >= DateTime.Now.AddMinutes(-5))
.CountAsync()
};
return Json(analytics);
}
[HttpGet]
public async Task<JsonResult> GetSurveyPerformanceData()
{
var performanceData = await _context.Questionnaires
.Include(q => q.Questions)
.Select(q => new
{
Name = q.Title,
Responses = _context.Responses.Count(r => r.QuestionnaireId == q.Id),
Questions = q.Questions.Count(),
CompletionRate = _context.Responses.Count(r => r.QuestionnaireId == q.Id) > 0
? Math.Round((_context.Responses.Count(r => r.QuestionnaireId == q.Id) / 10.0) * 100, 1)
: 0
})
.OrderByDescending(s => s.Responses)
.Take(10)
.ToListAsync();
return Json(performanceData);
}
[HttpGet]
public async Task<JsonResult> GetResponseTrendsData()
{
var thirtyDaysAgo = DateTime.Now.AddDays(-30);
var trendData = await _context.Responses
.Where(r => r.SubmissionDate >= thirtyDaysAgo)
.GroupBy(r => r.SubmissionDate.Date)
.Select(g => new
{
Date = g.Key.ToString("yyyy-MM-dd"),
Responses = g.Count()
})
.OrderBy(d => d.Date)
.ToListAsync();
return Json(trendData);
}
[HttpGet]
public async Task<JsonResult> GetQuestionTypeDistribution()
{
var distribution = await _context.Questions
.GroupBy(q => q.Type)
.Select(g => new
{
Type = g.Key.ToString(),
Count = g.Count()
})
.ToListAsync();
return Json(distribution);
}
// KEEP YOUR EXISTING METHODS UNCHANGED
[HttpGet]
public JsonResult GetVisitorData()
{
var visitorData = new List<VisitorDataViewModel>
{
new VisitorDataViewModel { Time = DateTime.Now.ToString("HH:mm:ss"), VisitorCount = new Random().Next(0, 500) },
new VisitorDataViewModel { Time = DateTime.Now.AddSeconds(-5).ToString("HH:mm:ss"), VisitorCount = new Random().Next(0, 500) },
new VisitorDataViewModel { Time = DateTime.Now.AddSeconds(-10).ToString("HH:mm:ss"), VisitorCount = new Random().Next(0, 500) }
};
return Json(visitorData);
}
[HttpGet]
public JsonResult GetPerformanceData()
{
var performanceData = new List<PerformanceDataViewModel>
{
new PerformanceDataViewModel { Time = DateTime.Now.ToString("HH:mm:ss"), CPUUsage = new Random().Next(0, 100), MemoryUsage = new Random().Next(0, 100) },
new PerformanceDataViewModel { Time = DateTime.Now.AddSeconds(-5).ToString("HH:mm:ss"), CPUUsage = new Random().Next(0, 100), MemoryUsage = new Random().Next(0, 100) },
new PerformanceDataViewModel { Time = DateTime.Now.AddSeconds(-10).ToString("HH:mm:ss"), CPUUsage = new Random().Next(0, 100), MemoryUsage = new Random().Next(0, 100) }
};
return Json(performanceData);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
return RedirectToAction("Login", "Account", new { area = "" });
}
}
}