Spaces:
Running
Running
task-tracking-system / TaskTrackingSystem.WebApp /Components /Pages /Features /Audit /AuditLogs.razor
| @page "/audit-logs" | |
| @using Microsoft.AspNetCore.Authorization | |
| @using Microsoft.AspNetCore.Components.Authorization | |
| @using TaskTrackingSystem.Shared.Models.AuditLog | |
| @using TaskTrackingSystem.WebApp.Components.Partial | |
| @rendermode @(new InteractiveServerRenderMode(prerender: false)) | |
| @attribute [Authorize] | |
| @inject ApiClientService ApiClient | |
| @inject IJSRuntime JS | |
| @inject AuthenticationStateProvider AuthStateProvider | |
| <PageTitle>@AppLocalization.PageTitle("auditLogs", "Audit Logs")</PageTitle> | |
| <div class="space-y-6"> | |
| <!-- Page Header --> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.PageTitle("auditLogs", "Audit Logs")</h2> | |
| <p class="text-slate-500 mt-1">@AppLocalization.PageDescription("auditLogs", "Review system activity and changes.")</p> | |
| </div> | |
| </div> | |
| <!-- Search Panel --> | |
| <div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm mb-4"> | |
| <div class="flex items-center gap-3"> | |
| <div class="relative flex-1 max-w-md"> | |
| <input @bind="searchInput" @bind:event="oninput" @onkeyup="HandleKeyUp" type="text" placeholder="@AppLocalization.Text("common.searchByUser", "Search by user, action, module, or details...")" | |
| class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" /> | |
| </div> | |
| <button @onclick="ApplySearch" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm"> | |
| <i data-lucide="search" class="h-4 w-4 mr-2"></i> | |
| @AppLocalization.Text("action.search", "Search") | |
| </button> | |
| <button @onclick="ResetSearch" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm"> | |
| <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i> | |
| @AppLocalization.Text("action.reset", "Reset") | |
| </button> | |
| </div> | |
| </div> | |
| @if (isLoading) | |
| { | |
| <LoadingSpinner Message="@AppLocalization.Text("common.loadingAuditLogs", "Loading audit logs...")" /> | |
| } | |
| else | |
| { | |
| <!-- Logs Table --> | |
| <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden"> | |
| <table class="w-full"> | |
| <thead> | |
| <tr class="border-b border-slate-100 bg-slate-50/60"> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.date", "Timestamp")</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.user", "User")</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.module", "Module")</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.actions", "Action")</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.description", "Description")</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">IP Address</th> | |
| </tr> | |
| </thead> | |
| <tbody class="divide-y divide-slate-100 font-sans"> | |
| @if (PagedLogs.Any()) | |
| { | |
| @foreach (var entry in PagedLogs.Select((log, index) => (log, index))) | |
| { | |
| var log = entry.log; | |
| <tr class="hover:bg-slate-50/50 transition-colors"> | |
| <td class="px-6 py-4 text-sm font-semibold text-slate-500 align-top">@((currentPage - 1) * pageSize + entry.index + 1)</td> | |
| <td class="px-6 py-4 text-xs font-mono text-slate-500 whitespace-nowrap align-top"> | |
| @log.CreatedAt.ToString("dd-MM-yyyy HH:mm:ss") | |
| </td> | |
| <td class="px-6 py-4 align-top"> | |
| <div class="flex items-center gap-2"> | |
| <span class="flex h-6 w-6 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-[10px] font-bold shrink-0"> | |
| @(log.UserFullName.Length > 0 ? log.UserFullName[0].ToString().ToUpper() : "S") | |
| </span> | |
| <div> | |
| <p class="text-xs font-semibold text-slate-800">@log.UserFullName</p> | |
| <p class="text-[10px] text-slate-400">@@@log.Username</p> | |
| </div> | |
| </div> | |
| </td> | |
| <td class="px-6 py-4 align-top"> | |
| <span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold bg-slate-100 text-slate-700 border border-slate-200"> | |
| @log.Module | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 align-top"> | |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-bold @GetActionBadge(log.Action)"> | |
| @log.Action | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 text-xs text-slate-700 whitespace-normal break-words align-top"> | |
| @log.Description | |
| </td> | |
| <td class="px-6 py-4 text-xs font-mono text-slate-500 whitespace-nowrap align-top"> | |
| @(string.IsNullOrWhiteSpace(log.IpAddress) ? "N/A" : log.IpAddress) | |
| </td> | |
| </tr> | |
| } | |
| } | |
| else | |
| { | |
| <EmptyState ColSpan="7" Icon="history" Title="@AppLocalization.Text("common.noResults", "No audit logs")" Subtitle="@AppLocalization.Text("common.noResults", "No security events or administrator operations were found.")" /> | |
| } | |
| </tbody> | |
| </table> | |
| <!-- Pagination --> | |
| <Pagination CurrentPage="currentPage" | |
| TotalPages="TotalPages" | |
| PageSize="pageSize" | |
| TotalCount="TotalCount" | |
| OnPageChanged="HandlePageChanged" | |
| OnPageSizeChanged="HandlePageSizeChanged" /> | |
| </div> | |
| } | |
| </div> | |
| @code { | |
| private bool isLoading = true; | |
| private List<AuditLogDto> auditLogs = new(); | |
| private string searchInput = ""; | |
| // Pagination state | |
| private int currentPage = 1; | |
| private int pageSize = 15; | |
| private int totalCount = 0; | |
| private int totalPages = 0; | |
| private int TotalPages => totalPages; | |
| private int TotalCount => totalCount; | |
| private IEnumerable<AuditLogDto> PagedLogs => auditLogs; | |
| protected override async Task OnInitializedAsync() | |
| { | |
| await LoadLogs(); | |
| } | |
| protected override async Task OnAfterRenderAsync(bool firstRender) | |
| { | |
| await JS.InvokeVoidAsync("initIcons"); | |
| } | |
| private async Task LoadLogs() | |
| { | |
| isLoading = true; | |
| var client = ApiClient.CreateClient(); | |
| try | |
| { | |
| var url = BuildAuditLogsUrl(); | |
| var result = await client.GetFromJsonAsync<PagedResult<AuditLogDto>>(url, Serialization.CaseInsensitive); | |
| auditLogs = result?.Items ?? new(); | |
| totalCount = result?.TotalCount ?? 0; | |
| totalPages = result?.TotalPages ?? 0; | |
| currentPage = result?.Page ?? currentPage; | |
| pageSize = result?.PageSize ?? pageSize; | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"Load audit logs error: {ex.Message}"); | |
| } | |
| finally | |
| { | |
| isLoading = false; | |
| } | |
| } | |
| private void ApplySearch() | |
| { | |
| currentPage = 1; | |
| _ = LoadLogs(); | |
| } | |
| private void ResetSearch() | |
| { | |
| searchInput = ""; | |
| currentPage = 1; | |
| _ = LoadLogs(); | |
| } | |
| private void HandleKeyUp(KeyboardEventArgs e) | |
| { | |
| if (e.Key == "Enter") | |
| { | |
| ApplySearch(); | |
| } | |
| } | |
| private string BuildAuditLogsUrl() | |
| { | |
| var query = new List<string> | |
| { | |
| $"page={currentPage}", | |
| $"limit={pageSize}" | |
| }; | |
| if (!string.IsNullOrWhiteSpace(searchInput)) | |
| { | |
| query.Add($"search={Uri.EscapeDataString(searchInput.Trim())}"); | |
| } | |
| return $"AuditLog?{string.Join("&", query)}"; | |
| } | |
| private async Task HandlePageChanged(int page) | |
| { | |
| currentPage = page; | |
| await LoadLogs(); | |
| } | |
| private async Task HandlePageSizeChanged(int size) | |
| { | |
| pageSize = size; | |
| currentPage = 1; | |
| await LoadLogs(); | |
| } | |
| private string GetActionBadge(string action) | |
| { | |
| return action.ToLower() switch | |
| { | |
| "create" => "bg-emerald-50 text-emerald-700 border border-emerald-200", | |
| "update" => "bg-amber-50 text-amber-700 border border-amber-200", | |
| "delete" => "bg-rose-50 text-rose-700 border border-rose-200", | |
| "assignaccess" => "bg-violet-50 text-violet-700 border border-violet-200", | |
| _ => "bg-blue-50 text-blue-700 border border-blue-200" | |
| }; | |
| } | |
| } | |