File size: 10,492 Bytes
121df90
 
 
 
 
 
ecb51fa
121df90
 
 
 
8c88a89
121df90
 
 
 
 
8c88a89
 
121df90
 
 
 
 
 
 
8c88a89
121df90
 
 
 
8c88a89
121df90
 
 
8c88a89
121df90
 
 
 
 
 
8c88a89
121df90
 
 
 
 
 
 
 
 
8c88a89
 
 
 
 
121df90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c88a89
121df90
 
 
 
 
 
 
 
7781da8
121df90
 
 
 
 
 
 
 
 
 
 
 
 
 
7781da8
 
 
 
 
121df90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7781da8
 
 
 
 
 
 
121df90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7781da8
 
 
 
 
 
 
121df90
7781da8
 
 
 
121df90
7781da8
 
121df90
7781da8
121df90
 
7781da8
121df90
 
7781da8
121df90
 
 
7781da8
121df90
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
@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"
        };
    }
}