| @inject ContactManagementAPI.Services.UserContextService UserContext |
| @model IEnumerable<ContactManagementAPI.Models.Contact> |
| @using System.Linq |
| @{ |
| ViewData["Title"] = "All Contacts"; |
| var searchTerm = ViewBag.SearchTerm ?? ""; |
|
|
| |
| var sortedModel = Model.OrderBy(c => ((c.FirstName ?? "").Trim() + " " + (c.LastName ?? "").Trim()).Trim()); |
| var listedCount = sortedModel.Count(); |
| } |
|
|
| @Html.AntiForgeryToken() |
|
|
| <div class="contacts-container"> |
| @if (TempData["SuccessMessage"] != null) |
| { |
| <div class="alert alert-success" style="padding: 15px; margin-bottom: 20px; background: #d4edda; border: 1px solid #c3e6cb; border-radius: 8px; color: #155724;"> |
| <i class="fas fa-check-circle"></i> @TempData["SuccessMessage"] |
| </div> |
| } |
| |
| @if (TempData["ErrorMessage"] != null) |
| { |
| <div class="alert alert-danger" style="padding: 15px; margin-bottom: 20px; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 8px; color: #721c24;"> |
| <i class="fas fa-exclamation-circle"></i> @TempData["ErrorMessage"] |
| </div> |
| } |
| |
| <div class="contacts-header"> |
| <h2>All Contacts</h2> |
| <div style="display: flex; gap: 10px; flex-wrap: wrap;"> |
| <a href="/home/dashboard" class="btn btn-secondary"> |
| <i class="fas fa-chart-line"></i> Dashboard |
| </a> |
| <a href="/home/findduplicates" class="btn btn-warning"> |
| <i class="fas fa-copy"></i> Find Duplicates |
| </a> |
| @if (UserContext.HasRight(RightsCatalog.ContactsCreate)) |
| { |
| <a href="/home/create" class="btn btn-primary"> |
| <i class="fas fa-plus"></i> Add New Contact |
| </a> |
| <a href="/home/import" class="btn btn-success"> |
| <i class="fas fa-file-import"></i> Import |
| </a> |
| } |
| @if (UserContext.HasRight(RightsCatalog.ContactsView)) |
| { |
| <div class="btn-group"> |
| <button type="button" class="btn btn-info dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> |
| <i class="fas fa-file-export"></i> Export |
| </button> |
| <ul class="dropdown-menu"> |
| <li><a class="dropdown-item" href="/home/exportexcel"><i class="fas fa-file-excel text-success"></i> Export to Excel</a></li> |
| <li><a class="dropdown-item" href="/home/exportcsv"><i class="fas fa-file-csv text-info"></i> Export to CSV</a></li> |
| <li><a class="dropdown-item" href="/home/exportpdf"><i class="fas fa-file-pdf text-danger"></i> Export to PDF</a></li> |
| </ul> |
| </div> |
| } |
| </div> |
| </div> |
|
|
| <div class="search-container" style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px;"> |
| <form method="get" action="/home/index" class="search-form"> |
| <div style="display: flex; gap: 10px; align-items: center;"> |
| <div style="flex: 1;"> |
| <input type="text" name="searchTerm" placeholder="Search by name, email, or phone..." |
| class="search-input" value="@searchTerm" |
| style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px;"> |
| </div> |
| <button type="submit" class="btn btn-info" style="white-space: nowrap;"> |
| <i class="fas fa-search"></i> Search |
| </button> |
| @if (!string.IsNullOrEmpty(searchTerm)) |
| { |
| <a href="/home/index" class="btn btn-secondary" style="white-space: nowrap;"> |
| <i class="fas fa-times"></i> Clear |
| </a> |
| } |
| </div> |
| </form> |
| @if (!string.IsNullOrEmpty(searchTerm)) |
| { |
| <p style="margin-top: 10px; color: #666; font-size: 13px;"> |
| <i class="fas fa-filter"></i> Showing results for "<strong>@searchTerm</strong>" |
| </p> |
| } |
| </div> |
|
|
| @if (!sortedModel.Any()) |
| { |
| <div class="no-contacts"> |
| <i class="fas fa-inbox"></i> |
| <p>No contacts found. |
| @if (UserContext.HasRight(RightsCatalog.ContactsCreate)) |
| { |
| <a href="/home/create">Create your first contact</a> |
| } |
| </p> |
| </div> |
| } |
| else |
| { |
| @if (UserContext.HasRight(RightsCatalog.ContactsDelete)) |
| { |
| <div class="bulk-actions" style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px; display: none;" id="bulkActionsBar"> |
| <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;"> |
| <span id="selectedCount" style="font-weight: 600; color: #495057;"> |
| <i class="fas fa-check-square"></i> <span id="countText">0</span> selected |
| </span> |
| <button type="button" class="btn btn-danger" id="deleteSelectedBtn"> |
| <i class="fas fa-trash"></i> Delete Selected |
| </button> |
| </div> |
| </div> |
| } |
|
|
| <style> |
| /* Contacts grid styling: header, zebra rows and hover (use high specificity to override Bootstrap) */ |
| .contacts-table thead th { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; |
| color: #ffffff !important; |
| border-bottom: 2px solid rgba(0,0,0,0.06) !important; |
| } |
| .contacts-table thead th .form-check-input { transform: scale(1.05); margin-top: 0; } |
|
|
| .contacts-table tbody tr:nth-of-type(odd) td { background-color: #f3f7fb !important; color: #212529 !important; } |
| .contacts-table tbody tr:nth-of-type(even) td { background-color: #ffffff !important; color: #212529 !important; } |
| .contacts-table tbody tr:hover td { background-color: #eef4ff !important; color: #212529 !important; } |
| .contacts-table th, .contacts-table td { vertical-align: middle; } |
| </style> |
|
|
| <style> |
| /* Responsive: hide table on small, show cards instead */ |
| @@media (max-width: 767.98px) { |
| .contacts-table-wrapper { display: none !important; } |
| .contacts-cards { display: block !important; } |
| } |
| @@media (min-width: 768px) { |
| .contacts-cards { display: none !important; } |
| } |
| </style> |
|
|
| <div class="contacts-table-wrapper"> |
| <div class="table-responsive"> |
| <table class="table table-hover table-striped contacts-table" style="background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);"> |
| <thead style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;"> |
| <tr> |
| @if (UserContext.HasRight(RightsCatalog.ContactsDelete)) |
| { |
| <th style="width: 50px; text-align: center;"> |
| <input type="checkbox" id="selectAll" class="form-check-input" style="cursor: pointer;" title="Select All"> |
| </th> |
| } |
| <th style="width: 60px;">Photo</th> |
| <th>Name</th> |
| <th>Email</th> |
| <th>Phone</th> |
| <th>WhatsApp</th> |
| <th>City</th> |
| <th>Group</th> |
| <th style="width: 180px; text-align: center;">Actions</th> |
| </tr> |
| </thead> |
| <tbody> |
| @{ var contactsList = sortedModel.ToList(); } |
| @for (var __i = 0; __i < contactsList.Count; __i++) |
| { |
| var contact = contactsList[__i]; |
| var _rowBg = (__i % 2 == 0) ? "#f3f7fb" : "#ffffff"; |
| |
| <tr style="background-color:@_rowBg;"> |
| @if (UserContext.HasRight(RightsCatalog.ContactsDelete)) |
| { |
| <td style="text-align: center; vertical-align: middle;"> |
| <input type="checkbox" class="contact-checkbox form-check-input" data-contact-id="@contact.Id" style="cursor: pointer;"> |
| </td> |
| } |
| <td style="vertical-align: middle;"> |
| @if (!string.IsNullOrEmpty(contact.PhotoPath)) |
| { |
| <img src="@contact.PhotoPath" alt="@contact.FirstName @contact.LastName" |
| style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover; border: 2px solid #dee2e6;" /> |
| } |
| else |
| { |
| <div style="width: 40px; height: 40px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold;"> |
| @contact.FirstName?.Substring(0, 1)@contact.LastName?.Substring(0, 1) |
| </div> |
| } |
| </td> |
| <td style="vertical-align: middle;"> |
| <strong>@contact.FirstName @contact.LastName</strong> |
| @if (!string.IsNullOrEmpty(contact.NickName)) |
| { |
| <br/><small class="text-muted">(@contact.NickName)</small> |
| } |
| </td> |
| <td style="vertical-align: middle;"> |
| @if (!string.IsNullOrEmpty(contact.Email)) |
| { |
| <a href="mailto:@contact.Email" style="text-decoration: none;"> |
| <i class="fas fa-envelope text-primary"></i> @contact.Email |
| </a> |
| } |
| else |
| { |
| <span class="text-muted">-</span> |
| } |
| </td> |
| <td style="vertical-align: middle;"> |
| @if (!string.IsNullOrEmpty(contact.Mobile1)) |
| { |
| <i class="fas fa-phone text-success"></i> @contact.Mobile1 |
| } |
| else |
| { |
| <span class="text-muted">-</span> |
| } |
| </td> |
| <td style="vertical-align: middle;"> |
| @if (!string.IsNullOrEmpty(contact.WhatsAppNumber)) |
| { |
| <a href="https://wa.me/@contact.WhatsAppNumber.Replace("+", "").Replace("-", "").Replace(" ", "")" |
| target="_blank" style="text-decoration: none;"> |
| <i class="fab fa-whatsapp text-success"></i> @contact.WhatsAppNumber |
| </a> |
| } |
| else |
| { |
| <span class="text-muted">-</span> |
| } |
| </td> |
| <td style="vertical-align: middle;"> |
| @if (!string.IsNullOrEmpty(contact.City)) |
| { |
| <i class="fas fa-map-marker-alt text-danger"></i> @contact.City |
| } |
| else |
| { |
| <span class="text-muted">-</span> |
| } |
| </td> |
| <td style="vertical-align: middle;"> |
| @if (contact.Group != null) |
| { |
| <span class="badge bg-info">@contact.Group.Name</span> |
| } |
| else |
| { |
| <span class="badge bg-secondary">Unassigned</span> |
| } |
| </td> |
| <td style="text-align: center; vertical-align: middle;"> |
| <div class="btn-group" role="group"> |
| <a href="/home/details/@contact.Id" class="btn btn-sm btn-info" title="View Details"> |
| <i class="fas fa-eye"></i> |
| </a> |
| @if (UserContext.HasRight(RightsCatalog.ContactsEdit)) |
| { |
| <a href="/home/edit/@contact.Id" class="btn btn-sm btn-warning" title="Edit"> |
| <i class="fas fa-edit"></i> |
| </a> |
| } |
| @if (UserContext.HasRight(RightsCatalog.ContactsDelete)) |
| { |
| <a href="/home/delete/@contact.Id" class="btn btn-sm btn-danger" title="Delete"> |
| <i class="fas fa-trash"></i> |
| </a> |
| } |
| </div> |
| </td> |
| </tr> |
| } |
| </tbody> |
| </table> |
| </div> |
| </div> |
|
|
| <!-- Mobile / Tablet: card view (visible on small screens) --> |
| <div class="contacts-cards" style="display:none;"> |
| @foreach (var contact in sortedModel) |
| { |
| <div class="card mb-2"> |
| <div class="card-body d-flex align-items-center" style="gap:12px;"> |
| @if (UserContext.HasRight(RightsCatalog.ContactsDelete)) |
| { |
| <div style="flex: 0 0 auto;"> |
| <input type="checkbox" class="contact-checkbox form-check-input" data-contact-id="@contact.Id" style="cursor: pointer;" /> |
| </div> |
| } |
|
|
| <div style="flex: 0 0 auto;"> |
| @if (!string.IsNullOrEmpty(contact.PhotoPath)) |
| { |
| <img src="@contact.PhotoPath" alt="@contact.FirstName @contact.LastName" style="width:48px;height:48px;border-radius:50%;object-fit:cover;border:2px solid #dee2e6;" /> |
| } |
| else |
| { |
| <div style="width:48px;height:48px;border-radius:50%;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);display:flex;align-items:center;justify-content:center;color:white;font-weight:bold;"> |
| @contact.FirstName?.Substring(0,1)@contact.LastName?.Substring(0,1) |
| </div> |
| } |
| </div> |
|
|
| <div style="flex:1; min-width:0;"> |
| <div style="font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;"> |
| @contact.FirstName @contact.LastName |
| </div> |
| <div style="font-size:13px; color:#6c757d;"> |
| @if (!string.IsNullOrEmpty(contact.Email)) { <span>@contact.Email</span> } |
| else if (!string.IsNullOrEmpty(contact.Mobile1)) { <span>@contact.Mobile1</span> } |
| </div> |
| </div> |
|
|
| <div style="flex:0 0 auto; display:flex; gap:6px;"> |
| <a href="/home/details/@contact.Id" class="btn btn-sm btn-info" title="View"><i class="fas fa-eye"></i></a> |
| @if (UserContext.HasRight(RightsCatalog.ContactsEdit)) |
| { |
| <a href="/home/edit/@contact.Id" class="btn btn-sm btn-warning" title="Edit"><i class="fas fa-edit"></i></a> |
| } |
| @if (UserContext.HasRight(RightsCatalog.ContactsDelete)) |
| { |
| <a href="/home/delete/@contact.Id" class="btn btn-sm btn-danger" title="Delete"><i class="fas fa-trash"></i></a> |
| } |
| </div> |
| </div> |
| </div> |
| } |
| </div> |
|
|
| <div id="contactsCountLabel" class="text-muted" style="margin-top: 10px; font-size: 13px;"> |
| Showing <strong>@listedCount</strong> contact(s) |
| @if (!string.IsNullOrEmpty(searchTerm)) |
| { |
| <span>for "<strong>@searchTerm</strong>"</span> |
| } |
| . |
| </div> |
| } |
| </div> |
|
|
| @section Scripts { |
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| const selectAllCheckbox = document.getElementById('selectAll'); |
| const bulkActionsBar = document.getElementById('bulkActionsBar'); |
| const deleteSelectedBtn = document.getElementById('deleteSelectedBtn'); |
| const countText = document.getElementById('countText'); |
|
|
| // Helper to get the current set of contact checkboxes (live) |
| function getContactCheckboxes() { |
| return document.querySelectorAll('.contact-checkbox'); |
| } |
|
|
| // Select All functionality (use live query so we always act on current DOM) |
| if (selectAllCheckbox) { |
| selectAllCheckbox.addEventListener('change', function() { |
| const checked = this.checked; |
| const boxes = getContactCheckboxes(); |
| boxes.forEach(checkbox => { |
| checkbox.checked = checked; |
| }); |
| // ensure indeterminate cleared when user toggles header |
| this.indeterminate = false; |
| updateBulkActionsBar(); |
| }); |
| } |
|
|
| // Use event delegation for individual checkbox changes (reliable for dynamic content) |
| document.addEventListener('change', function(e) { |
| if (e.target && e.target.classList && e.target.classList.contains('contact-checkbox')) { |
| updateSelectAllState(); |
| updateBulkActionsBar(); |
| } |
| }); |
|
|
| // Update Select All checkbox state |
| function updateSelectAllState() { |
| if (!selectAllCheckbox) return; |
|
|
| const totalCheckboxes = getContactCheckboxes().length; |
| const checkedCheckboxes = document.querySelectorAll('.contact-checkbox:checked').length; |
|
|
| selectAllCheckbox.checked = (totalCheckboxes > 0) && (totalCheckboxes === checkedCheckboxes); |
| selectAllCheckbox.indeterminate = checkedCheckboxes > 0 && checkedCheckboxes < totalCheckboxes; |
| } |
|
|
| // Update bulk actions bar visibility |
| function updateBulkActionsBar() { |
| const checkedCount = document.querySelectorAll('.contact-checkbox:checked').length; |
|
|
| if (checkedCount > 0) { |
| if (bulkActionsBar) bulkActionsBar.style.display = 'block'; |
| if (countText) countText.textContent = checkedCount; |
| } else { |
| if (bulkActionsBar) bulkActionsBar.style.display = 'none'; |
| } |
| // keep header checkbox state in sync after updating UI |
| updateSelectAllState(); |
| } |
|
|
| // Delete selected contacts |
| if (deleteSelectedBtn) { |
| deleteSelectedBtn.addEventListener('click', function() { |
| const selectedIds = Array.from(document.querySelectorAll('.contact-checkbox:checked')) |
| .map(checkbox => checkbox.getAttribute('data-contact-id')); |
| |
| if (selectedIds.length === 0) { |
| alert('Please select at least one contact to delete.'); |
| return; |
| } |
|
|
| const confirmMessage = `Are you sure you want to delete ${selectedIds.length} contact(s)?\n\nThis action cannot be undone!`; |
| |
| if (confirm(confirmMessage)) { |
| // Create form and submit |
| const form = document.createElement('form'); |
| form.method = 'POST'; |
| form.action = '/home/deletemultiple'; |
| |
| // Add anti-forgery token |
| const token = document.querySelector('input[name="__RequestVerificationToken"]'); |
| if (token) { |
| const tokenInput = document.createElement('input'); |
| tokenInput.type = 'hidden'; |
| tokenInput.name = '__RequestVerificationToken'; |
| tokenInput.value = token.value; |
| form.appendChild(tokenInput); |
| } |
| |
| // Add contact IDs |
| selectedIds.forEach(id => { |
| const input = document.createElement('input'); |
| input.type = 'hidden'; |
| input.name = 'contactIds'; |
| input.value = id; |
| form.appendChild(input); |
| }); |
| |
| document.body.appendChild(form); |
| form.submit(); |
| } |
| }); |
| } |
| }); |
| </script> |
| } |
|
|