using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using ContactManagementAPI.Data; using ContactManagementAPI.Models; using ContactManagementAPI.Services; using ContactManagementAPI.Security; using System.Globalization; namespace ContactManagementAPI.Controllers { public class HomeController : Controller { private readonly ApplicationDbContext _context; private readonly FileUploadService _fileUploadService; private readonly ImportExportService _importExportService; private readonly ContactStatisticsService _statisticsService; private readonly UserContextService _userContextService; private readonly AdminHistoryService _adminHistoryService; public HomeController( ApplicationDbContext context, FileUploadService fileUploadService, ImportExportService importExportService, ContactStatisticsService statisticsService, UserContextService userContextService, AdminHistoryService adminHistoryService) { _context = context; _fileUploadService = fileUploadService; _importExportService = importExportService; _statisticsService = statisticsService; _userContextService = userContextService; _adminHistoryService = adminHistoryService; } // GET: Home/Index - Display all contacts with search functionality [RequireRight(RightsCatalog.ContactsView)] public async Task Index(string searchTerm = "") { var currentUser = _userContextService.CurrentUser; if (currentUser == null) { return RedirectToAction("Login", "Account"); } var contacts = ApplyContactScope( _context.Contacts .Include(c => c.Group) .AsQueryable(), currentUser) .AsQueryable(); if (!string.IsNullOrEmpty(searchTerm)) { contacts = contacts.Where(c => c.FirstName.Contains(searchTerm) || (c.LastName != null && c.LastName.Contains(searchTerm)) || (c.Email != null && c.Email.Contains(searchTerm)) || (c.Mobile1 != null && c.Mobile1.Contains(searchTerm)) || (c.Mobile2 != null && c.Mobile2.Contains(searchTerm)) || (c.Mobile3 != null && c.Mobile3.Contains(searchTerm))); } ViewBag.SearchTerm = searchTerm; return View(await contacts.OrderByDescending(c => c.UpdatedAt).ToListAsync()); } // GET: Home/Details/5 [RequireRight(RightsCatalog.ContactsView)] public async Task Details(int? id) { if (id == null) return NotFound(); var currentUser = _userContextService.CurrentUser; if (currentUser == null) { return RedirectToAction("Login", "Account"); } var contact = await _context.Contacts .Include(c => c.Group) .Include(c => c.Photos) .Include(c => c.Documents) .Include(c => c.BankAccounts) .FirstOrDefaultAsync(c => c.Id == id); if (contact == null) return NotFound(); if (!CanAccessContact(currentUser, contact)) { TempData["ErrorMessage"] = "You can view only contacts from your group."; return RedirectToAction(nameof(Index)); } return View(contact); } // GET: Home/Create [RequireRight(RightsCatalog.ContactsCreate)] public IActionResult Create() { var currentUser = _userContextService.CurrentUser; if (currentUser == null) { return RedirectToAction("Login", "Account"); } PopulateFormData(); if (!currentUser.IsAdmin) { var scopedContactGroupId = ResolveContactGroupIdForUser(currentUser); if (scopedContactGroupId.HasValue) { ViewData["ForcedGroupId"] = scopedContactGroupId.Value; } } return View(); } // POST: Home/Create [HttpPost] [ValidateAntiForgeryToken] [RequireRight(RightsCatalog.ContactsCreate)] public async Task Create([Bind("FirstName,LastName,NickName,Gender,DateOfBirth,Email,Mobile1,Mobile2,Mobile3,WhatsAppNumber,PassportNumber,PanNumber,AadharNumber,DrivingLicenseNumber,VotersId,Address,City,State,PostalCode,Country,GroupId,OtherDetails")] Contact contact, List? bankAccounts, IFormFile? profilePhoto) { var currentUser = _userContextService.CurrentUser; if (currentUser == null) { return RedirectToAction("Login", "Account"); } if (!currentUser.IsAdmin) { var scopedContactGroupId = ResolveContactGroupIdForUser(currentUser); if (!scopedContactGroupId.HasValue) { ModelState.AddModelError(nameof(Contact.GroupId), "Your account is not assigned to a contact group."); } else { contact.GroupId = scopedContactGroupId.Value; } } NormalizeOptionalBankAccountModelState(); ValidateDuplicateContact(contact); if (ModelState.IsValid) { contact.CreatedAt = DateTime.Now; contact.UpdatedAt = DateTime.Now; var preparedBankAccounts = PrepareBankAccounts(bankAccounts); SyncLegacyBankFields(contact, preparedBankAccounts.FirstOrDefault()); // Save contact first to get the ID _context.Add(contact); await _context.SaveChangesAsync(); if (preparedBankAccounts.Any()) { foreach (var bankAccount in preparedBankAccounts) { bankAccount.ContactId = contact.Id; } _context.ContactBankAccounts.AddRange(preparedBankAccounts); await _context.SaveChangesAsync(); } // Handle profile photo upload after we have the contact ID if (profilePhoto != null) { var result = await _fileUploadService.UploadPhotoAsync(profilePhoto, contact.Id); if (result.Success) { contact.PhotoPath = result.FilePath; _context.Update(contact); await _context.SaveChangesAsync(); } } _adminHistoryService.Log( actionType: "Create", entityType: "Contact", entityId: contact.Id, performedBy: _userContextService.CurrentUser?.UserName ?? "Unknown", details: $"Created contact '{contact.FirstName} {contact.LastName}'."); TempData["SuccessMessage"] = "Contact created successfully!"; return RedirectToAction(nameof(Details), new { id = contact.Id }); } PopulateFormData(contact, PrepareBankAccounts(bankAccounts)); return View(contact); } // GET: Home/Edit/5 [RequireRight(RightsCatalog.ContactsEdit)] public async Task Edit(int? id) { if (id == null) return NotFound(); var currentUser = _userContextService.CurrentUser; if (currentUser == null) { return RedirectToAction("Login", "Account"); } if (!currentUser.IsAdmin) { TempData["ErrorMessage"] = "Only admin can edit existing contacts."; return RedirectToAction(nameof(Index)); } var contact = await _context.Contacts .Include(c => c.BankAccounts) .FirstOrDefaultAsync(c => c.Id == id); if (contact == null) return NotFound(); PopulateFormData(contact); return View(contact); } // POST: Home/Edit/5 [HttpPost] [ValidateAntiForgeryToken] [RequireRight(RightsCatalog.ContactsEdit)] public async Task Edit(int id, List? bankAccounts, IFormFile? profilePhoto) { var currentUser = _userContextService.CurrentUser; if (currentUser == null) { return RedirectToAction("Login", "Account"); } if (!currentUser.IsAdmin) { TempData["ErrorMessage"] = "Only admin can edit existing contacts."; return RedirectToAction(nameof(Index)); } NormalizeOptionalBankAccountModelState(); var existingContact = await _context.Contacts .AsTracking() .Include(c => c.BankAccounts) .FirstOrDefaultAsync(c => c.Id == id); if (existingContact == null) return NotFound(); var updateSucceeded = await TryUpdateModelAsync( existingContact, "", c => c.FirstName, c => c.LastName, c => c.NickName, c => c.Gender, c => c.DateOfBirth, c => c.Email, c => c.Mobile1, c => c.Mobile2, c => c.Mobile3, c => c.WhatsAppNumber, c => c.PassportNumber, c => c.PanNumber, c => c.AadharNumber, c => c.DrivingLicenseNumber, c => c.VotersId, c => c.Address, c => c.City, c => c.State, c => c.PostalCode, c => c.Country, c => c.GroupId, c => c.OtherDetails); if (!updateSucceeded) { PopulateFormData(existingContact, PrepareBankAccounts(bankAccounts)); return View(existingContact); } ValidateDuplicateContact(existingContact, id); if (ModelState.IsValid) { try { var postedGender = Request.Form["Gender"].ToString(); existingContact.Gender = string.IsNullOrWhiteSpace(postedGender) ? null : postedGender; var postedDateOfBirth = Request.Form["DateOfBirth"].ToString(); if (string.IsNullOrWhiteSpace(postedDateOfBirth)) { existingContact.DateOfBirth = null; } else if (DateTime.TryParseExact(postedDateOfBirth, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedDateOfBirth) || DateTime.TryParse(postedDateOfBirth, out parsedDateOfBirth)) { existingContact.DateOfBirth = parsedDateOfBirth; } existingContact.UpdatedAt = DateTime.Now; var preparedBankAccounts = PrepareBankAccounts(bankAccounts); if (existingContact.BankAccounts.Any()) { _context.ContactBankAccounts.RemoveRange(existingContact.BankAccounts); } if (preparedBankAccounts.Any()) { foreach (var bankAccount in preparedBankAccounts) { bankAccount.ContactId = existingContact.Id; } _context.ContactBankAccounts.AddRange(preparedBankAccounts); } SyncLegacyBankFields(existingContact, preparedBankAccounts.FirstOrDefault()); // Handle profile photo upload if (profilePhoto != null) { var result = await _fileUploadService.UploadPhotoAsync(profilePhoto, existingContact.Id); if (result.Success) { // Delete old photo if exists if (!string.IsNullOrEmpty(existingContact.PhotoPath)) { _fileUploadService.DeleteFile(existingContact.PhotoPath); } existingContact.PhotoPath = result.FilePath; } } await _context.SaveChangesAsync(); _adminHistoryService.Log( actionType: "Edit", entityType: "Contact", entityId: existingContact.Id, performedBy: _userContextService.CurrentUser?.UserName ?? "Unknown", details: $"Edited contact '{existingContact.FirstName} {existingContact.LastName}'."); TempData["SuccessMessage"] = "Contact updated successfully!"; return RedirectToAction(nameof(Details), new { id = existingContact.Id }); } catch (DbUpdateConcurrencyException) { if (!ContactExists(id)) return NotFound(); throw; } } PopulateFormData(existingContact, PrepareBankAccounts(bankAccounts)); return View(existingContact); } // GET: Home/Delete/5 [RequireRight(RightsCatalog.ContactsDelete)] public async Task Delete(int? id) { if (id == null) return NotFound(); var currentUser = _userContextService.CurrentUser; if (currentUser == null) { return RedirectToAction("Login", "Account"); } if (!currentUser.IsAdmin) { TempData["ErrorMessage"] = "Only admin can delete contacts."; return RedirectToAction(nameof(Index)); } var contact = await _context.Contacts .Include(c => c.Group) .FirstOrDefaultAsync(c => c.Id == id); if (contact == null) return NotFound(); return View(contact); } // POST: Home/Delete/5 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] [RequireRight(RightsCatalog.ContactsDelete)] public async Task DeleteConfirmed(int id) { var currentUser = _userContextService.CurrentUser; if (currentUser == null) { return RedirectToAction("Login", "Account"); } if (!currentUser.IsAdmin) { TempData["ErrorMessage"] = "Only admin can delete contacts."; return RedirectToAction(nameof(Index)); } var contact = await _context.Contacts.FindAsync(id); if (contact != null) { _context.Contacts.Remove(contact); await _context.SaveChangesAsync(); TempData["SuccessMessage"] = "Contact deleted successfully!"; } return RedirectToAction(nameof(Index)); } // POST: Home/DeleteMultiple - Bulk delete contacts [HttpPost] [ValidateAntiForgeryToken] [RequireRight(RightsCatalog.ContactsDelete)] public async Task DeleteMultiple(List contactIds) { var currentUser = _userContextService.CurrentUser; if (currentUser == null) { return RedirectToAction("Login", "Account"); } if (!currentUser.IsAdmin) { TempData["ErrorMessage"] = "Only admin can delete contacts."; return RedirectToAction(nameof(Index)); } if (contactIds == null || !contactIds.Any()) { TempData["ErrorMessage"] = "No contacts selected for deletion."; return RedirectToAction(nameof(Index)); } try { var contactsToDelete = await _context.Contacts .Where(c => contactIds.Contains(c.Id)) .ToListAsync(); if (contactsToDelete.Any()) { _context.Contacts.RemoveRange(contactsToDelete); await _context.SaveChangesAsync(); TempData["SuccessMessage"] = $"Successfully deleted {contactsToDelete.Count} contact(s)!"; } else { TempData["ErrorMessage"] = "No matching contacts found to delete."; } } catch (Exception ex) { TempData["ErrorMessage"] = $"Error deleting contacts: {ex.Message}"; } return RedirectToAction(nameof(Index)); } private bool ContactExists(int id) { return _context.Contacts.Any(e => e.Id == id); } private void PopulateFormData(Contact? contact = null, List? bankAccounts = null) { ViewData["Groups"] = _context.ContactGroups.OrderBy(g => g.Name).ToList(); var bankNames = _context.ContactBankAccounts .Where(b => !string.IsNullOrWhiteSpace(b.BankName)) .Select(b => b.BankName!) .Distinct() .OrderBy(name => name) .ToList(); if (!string.IsNullOrWhiteSpace(contact?.BankName) && !bankNames.Contains(contact.BankName)) { bankNames.Add(contact.BankName); bankNames = bankNames.OrderBy(name => name).ToList(); } ViewData["BankNames"] = bankNames; if (bankAccounts != null && bankAccounts.Any()) { ViewData["BankAccounts"] = bankAccounts; return; } if (contact?.BankAccounts != null && contact.BankAccounts.Any()) { ViewData["BankAccounts"] = contact.BankAccounts.OrderBy(b => b.Id).ToList(); return; } if (contact != null && (!string.IsNullOrWhiteSpace(contact.BankAccountNumber) || !string.IsNullOrWhiteSpace(contact.BankName) || !string.IsNullOrWhiteSpace(contact.BranchName) || !string.IsNullOrWhiteSpace(contact.IfscCode))) { ViewData["BankAccounts"] = new List { new ContactBankAccount { AccountNumber = contact.BankAccountNumber, BankName = contact.BankName, BranchName = contact.BranchName, IfscCode = contact.IfscCode } }; return; } ViewData["BankAccounts"] = new List { new ContactBankAccount() }; } private static List PrepareBankAccounts(List? bankAccounts) { return (bankAccounts ?? new List()) .Where(b => !string.IsNullOrWhiteSpace(b.AccountNumber) || !string.IsNullOrWhiteSpace(b.BankName) || !string.IsNullOrWhiteSpace(b.BranchName) || !string.IsNullOrWhiteSpace(b.IfscCode)) .Select(b => new ContactBankAccount { AccountNumber = b.AccountNumber, BankName = b.BankName, BranchName = b.BranchName, IfscCode = b.IfscCode, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }) .ToList(); } private static void SyncLegacyBankFields(Contact contact, ContactBankAccount? primaryBankAccount) { if (primaryBankAccount == null) { contact.BankAccountNumber = null; contact.BankName = null; contact.BranchName = null; contact.IfscCode = null; return; } contact.BankAccountNumber = primaryBankAccount.AccountNumber; contact.BankName = primaryBankAccount.BankName; contact.BranchName = primaryBankAccount.BranchName; contact.IfscCode = primaryBankAccount.IfscCode; } private void NormalizeOptionalBankAccountModelState() { var bankAccountKeys = ModelState.Keys .Where(key => key.StartsWith("bankAccounts[", StringComparison.OrdinalIgnoreCase)) .ToList(); foreach (var key in bankAccountKeys) { ModelState.Remove(key); } } private void ValidateDuplicateContact(Contact contact, int? excludeContactId = null) { var normalizedMobile = NormalizeDigits(contact.Mobile1); var normalizedAadhar = NormalizeDigits(contact.AadharNumber); if (!string.IsNullOrWhiteSpace(normalizedMobile)) { var mobileConflict = _context.Contacts .AsNoTracking() .Where(c => c.Id != (excludeContactId ?? 0) && !string.IsNullOrWhiteSpace(c.Mobile1)) .Select(c => new { c.FirstName, c.LastName, c.Mobile1 }) .ToList() .FirstOrDefault(c => NormalizeDigits(c.Mobile1) == normalizedMobile); if (mobileConflict != null) { ModelState.AddModelError(nameof(Contact.Mobile1), $"Mobile number already exists for contact '{mobileConflict.FirstName} {mobileConflict.LastName}'."); } } if (!string.IsNullOrWhiteSpace(normalizedAadhar)) { var aadharConflict = _context.Contacts .AsNoTracking() .Where(c => c.Id != (excludeContactId ?? 0) && !string.IsNullOrWhiteSpace(c.AadharNumber)) .Select(c => new { c.FirstName, c.LastName, c.AadharNumber }) .ToList() .FirstOrDefault(c => NormalizeDigits(c.AadharNumber) == normalizedAadhar); if (aadharConflict != null) { ModelState.AddModelError(nameof(Contact.AadharNumber), $"Aadhar number already exists for contact '{aadharConflict.FirstName} {aadharConflict.LastName}'."); } } if (string.IsNullOrWhiteSpace(normalizedMobile) && string.IsNullOrWhiteSpace(normalizedAadhar)) { var firstName = NormalizeText(contact.FirstName); var lastName = NormalizeText(contact.LastName); var nickName = NormalizeText(contact.NickName); if (!string.IsNullOrWhiteSpace(firstName)) { var nameConflict = _context.Contacts .AsNoTracking() .Where(c => c.Id != (excludeContactId ?? 0) && !string.IsNullOrWhiteSpace(c.FirstName)) .Select(c => new { c.FirstName, c.LastName, c.NickName }) .ToList() .FirstOrDefault(c => NormalizeText(c.FirstName) == firstName && NormalizeText(c.LastName) == lastName && NormalizeText(c.NickName) == nickName); if (nameConflict != null) { ModelState.AddModelError(nameof(Contact.FirstName), "A contact with the same First Name, Last Name, and Nick Name already exists. Provide Mobile1 or Aadhar if this is a different person."); } } } } private List FilterDuplicateImportedContacts(List contacts, List errors) { var validContacts = new List(); var existingContacts = _context.Contacts .AsNoTracking() .Select(c => new { c.FirstName, c.LastName, c.NickName, c.Mobile1, c.AadharNumber }) .ToList(); var seenMobiles = new HashSet(); var seenAadhars = new HashSet(); var seenNames = new HashSet(); for (var i = 0; i < contacts.Count; i++) { var contact = contacts[i]; var rowLabel = $"Row {i + 1} ({contact.FirstName} {contact.LastName})"; var normalizedMobile = NormalizeDigits(contact.Mobile1); var normalizedAadhar = NormalizeDigits(contact.AadharNumber); var firstName = NormalizeText(contact.FirstName); var lastName = NormalizeText(contact.LastName); var nickName = NormalizeText(contact.NickName); var nameKey = $"{firstName}|{lastName}|{nickName}"; var hasError = false; if (!string.IsNullOrWhiteSpace(normalizedMobile)) { var existsInDb = existingContacts.Any(c => NormalizeDigits(c.Mobile1) == normalizedMobile); var existsInImport = seenMobiles.Contains(normalizedMobile); if (existsInDb || existsInImport) { errors.Add($"{rowLabel}: Mobile1 already exists."); hasError = true; } } if (!string.IsNullOrWhiteSpace(normalizedAadhar)) { var existsInDb = existingContacts.Any(c => NormalizeDigits(c.AadharNumber) == normalizedAadhar); var existsInImport = seenAadhars.Contains(normalizedAadhar); if (existsInDb || existsInImport) { errors.Add($"{rowLabel}: AadharNumber already exists."); hasError = true; } } if (string.IsNullOrWhiteSpace(normalizedMobile) && string.IsNullOrWhiteSpace(normalizedAadhar)) { var existsInDb = existingContacts.Any(c => NormalizeText(c.FirstName) == firstName && NormalizeText(c.LastName) == lastName && NormalizeText(c.NickName) == nickName); var existsInImport = seenNames.Contains(nameKey); if (existsInDb || existsInImport) { errors.Add($"{rowLabel}: Same First Name + Last Name + Nick Name already exists."); hasError = true; } } if (hasError) { continue; } if (!string.IsNullOrWhiteSpace(normalizedMobile)) { seenMobiles.Add(normalizedMobile); } if (!string.IsNullOrWhiteSpace(normalizedAadhar)) { seenAadhars.Add(normalizedAadhar); } if (string.IsNullOrWhiteSpace(normalizedMobile) && string.IsNullOrWhiteSpace(normalizedAadhar)) { seenNames.Add(nameKey); } validContacts.Add(contact); } return validContacts; } private static string NormalizeDigits(string? value) { if (string.IsNullOrWhiteSpace(value)) { return string.Empty; } return new string(value.Where(char.IsDigit).ToArray()); } private static string NormalizeText(string? value) { return (value ?? string.Empty).Trim().ToUpperInvariant(); } #region Import/Export Actions // GET: Home/Dashboard - Display contact statistics [RequireRight(RightsCatalog.ContactsView)] public async Task Dashboard() { var currentUser = _userContextService.CurrentUser; if (currentUser == null) { return RedirectToAction("Login", "Account"); } if (!currentUser.IsAdmin) { TempData["ErrorMessage"] = "Dashboard is available only for admin."; return RedirectToAction(nameof(Index)); } var statistics = await _statisticsService.GetStatisticsAsync(); return View(statistics); } // GET: Home/FindDuplicates - Display potential duplicate contacts [RequireRight(RightsCatalog.ContactsView)] public async Task FindDuplicates() { var currentUser = _userContextService.CurrentUser; if (currentUser == null) { return RedirectToAction("Login", "Account"); } if (!currentUser.IsAdmin) { TempData["ErrorMessage"] = "Find Duplicates is available only for admin."; return RedirectToAction(nameof(Index)); } var duplicates = await _statisticsService.FindDuplicatesAsync(); return View(duplicates); } // GET: Home/Import - Display import page [RequireRight(RightsCatalog.ContactsCreate)] public IActionResult Import() { return View(); } // POST: Home/ImportFile - Handle file import [HttpPost] [ValidateAntiForgeryToken] [RequireRight(RightsCatalog.ContactsCreate)] public async Task ImportFile(IFormFile file, string fileType) { var currentUser = _userContextService.CurrentUser; if (currentUser == null) { return RedirectToAction("Login", "Account"); } if (file == null || file.Length == 0) { TempData["ErrorMessage"] = "Please select a file to import."; return RedirectToAction(nameof(Import)); } List contacts; List errors; try { using var stream = file.OpenReadStream(); if (fileType == "excel") { (contacts, errors) = await _importExportService.ImportFromExcel(stream); } else if (fileType == "csv") { (contacts, errors) = await _importExportService.ImportFromCsv(stream); } else { TempData["ErrorMessage"] = "Invalid file type selected."; return RedirectToAction(nameof(Import)); } if (errors.Any()) { TempData["ErrorMessage"] = $"Import completed with errors:
{string.Join("
", errors)}"; } if (contacts.Any()) { if (!currentUser.IsAdmin) { var scopedContactGroupId = ResolveContactGroupIdForUser(currentUser); if (!scopedContactGroupId.HasValue) { TempData["ErrorMessage"] = "Your account is not assigned to a contact group."; return RedirectToAction(nameof(Import)); } foreach (var importedContact in contacts) { importedContact.GroupId = scopedContactGroupId.Value; } } contacts = FilterDuplicateImportedContacts(contacts, errors); if (errors.Any()) { TempData["ErrorMessage"] = $"Import completed with errors:
{string.Join("
", errors)}"; } if (!contacts.Any()) { TempData["ErrorMessage"] = TempData["ErrorMessage"] ?? "No valid contacts found in the file."; return RedirectToAction(nameof(Import)); } // Set change tracking to true for saving _context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll; await _context.Contacts.AddRangeAsync(contacts); await _context.SaveChangesAsync(); // Reset tracking behavior _context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; TempData["SuccessMessage"] = $"Successfully imported {contacts.Count} contact(s)!"; } else { TempData["ErrorMessage"] = "No valid contacts found in the file."; } } catch (Exception ex) { TempData["ErrorMessage"] = $"Error importing file: {ex.Message}"; } return RedirectToAction(nameof(Index)); } // GET: Home/ExportExcel - Export to Excel [RequireRight(RightsCatalog.ContactsView)] public async Task ExportExcel() { var currentUser = _userContextService.CurrentUser; if (currentUser == null) { return RedirectToAction("Login", "Account"); } var contacts = await ApplyContactScope( _context.Contacts .Include(c => c.Group) .OrderBy(c => c.FirstName), currentUser) .ToListAsync(); var fileBytes = await _importExportService.ExportToExcel(contacts); var fileName = $"Contacts_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx"; return File(fileBytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName); } // GET: Home/ExportCsv - Export to CSV [RequireRight(RightsCatalog.ContactsView)] public async Task ExportCsv() { var currentUser = _userContextService.CurrentUser; if (currentUser == null) { return RedirectToAction("Login", "Account"); } var contacts = await ApplyContactScope( _context.Contacts .Include(c => c.Group) .OrderBy(c => c.FirstName), currentUser) .ToListAsync(); var fileBytes = await _importExportService.ExportToCsv(contacts); var fileName = $"Contacts_{DateTime.Now:yyyyMMdd_HHmmss}.csv"; return File(fileBytes, "text/csv", fileName); } // GET: Home/ExportPdf - Export to PDF [RequireRight(RightsCatalog.ContactsView)] public async Task ExportPdf() { var currentUser = _userContextService.CurrentUser; if (currentUser == null) { return RedirectToAction("Login", "Account"); } var contacts = await ApplyContactScope( _context.Contacts .Include(c => c.Group) .OrderBy(c => c.FirstName), currentUser) .ToListAsync(); var fileBytes = await _importExportService.ExportToPdf(contacts); var fileName = $"Contacts_{DateTime.Now:yyyyMMdd_HHmmss}.pdf"; return File(fileBytes, "application/pdf", fileName); } // GET: Home/DownloadTemplate - Download import template public async Task DownloadTemplate(string type) { if (type == "excel") { var fileBytes = await _importExportService.GenerateExcelTemplate(); return File(fileBytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "Contact_Import_Template.xlsx"); } else if (type == "csv") { var fileBytes = await _importExportService.GenerateCsvTemplate(); return File(fileBytes, "text/csv", "Contact_Import_Template.csv"); } return NotFound(); } private IQueryable ApplyContactScope(IQueryable query, AppUser currentUser) { if (currentUser.IsAdmin) { return query; } var scopedContactGroupId = ResolveContactGroupIdForUser(currentUser); if (!scopedContactGroupId.HasValue) { return query.Where(c => false); } var groupId = scopedContactGroupId.Value; return query.Where(c => c.GroupId == groupId); } private bool CanAccessContact(AppUser currentUser, Contact contact) { if (currentUser.IsAdmin) { return true; } var scopedContactGroupId = ResolveContactGroupIdForUser(currentUser); return scopedContactGroupId.HasValue && contact.GroupId == scopedContactGroupId.Value; } private int? ResolveContactGroupIdForUser(AppUser user) { if (user.IsAdmin) { return null; } if (user.GroupId <= 0) { return null; } var userGroupName = _context.UserGroups .Where(g => g.Id == user.GroupId) .Select(g => g.Name) .FirstOrDefault(); if (!string.IsNullOrWhiteSpace(userGroupName) && userGroupName.StartsWith("ContactGroup - ", StringComparison.OrdinalIgnoreCase)) { var contactGroupName = userGroupName.Substring("ContactGroup - ".Length).Trim(); var mappedContactGroupId = _context.ContactGroups .Where(cg => cg.Name == contactGroupName) .Select(cg => (int?)cg.Id) .FirstOrDefault(); if (mappedContactGroupId.HasValue) { return mappedContactGroupId.Value; } } var directMatch = _context.ContactGroups .Where(cg => cg.Id == user.GroupId) .Select(cg => (int?)cg.Id) .FirstOrDefault(); return directMatch; } #endregion } }