Spaces:
Sleeping
Sleeping
File size: 14,354 Bytes
cf599e0 b866d20 4433399 b866d20 cf599e0 b866d20 6aad87c b866d20 6aad87c b866d20 cf599e0 b866d20 6aad87c b866d20 6aad87c b866d20 6aad87c b866d20 6aad87c b866d20 46cf1b2 b866d20 6aad87c b866d20 6aad87c b866d20 6aad87c b866d20 6aad87c b866d20 6aad87c b866d20 6aad87c b866d20 6aad87c b866d20 6aad87c b866d20 6aad87c b866d20 6aad87c b866d20 46cf1b2 b866d20 46cf1b2 b866d20 46cf1b2 b866d20 4433399 | 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 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | @page "/Borrowings/Manage"
@using BlazorFrontend.Services
@using LibraryManagement.Shared.Models
@using Microsoft.AspNetCore.Authorization
@inject LibraryApiClient ApiClient
@inject IJSRuntime JS
@attribute [Authorize(Roles = "Librarian")]
<PageTitle>Manage Borrowings</PageTitle>
<div class="mb-10 animate-in fade-in slide-in-from-top-4 duration-500">
<div class="flex items-center justify-between mb-4">
<div>
<h1 class="text-3xl font-extrabold tracking-tight text-mystic-900 border-b border-mystic-100 pb-4 mb-2">Manage All Borrowings</h1>
<p class="text-mystic-500 font-medium">Monitor and process library loans across all members.</p>
</div>
<div class="flex gap-3">
<div class="card px-4 py-2 flex items-center gap-2 bg-mystic-50 border-mystic-200">
<span class="text-xs font-bold text-mystic-400 uppercase tracking-widest">Total Active:</span>
<span class="text-lg font-bold text-mystic-900">@_borrowings.Count(m => m.Status == "Borrowed")</span>
</div>
</div>
</div>
</div>
@if (_isLoading)
{
<div class="py-20 flex justify-center">
<div class="animate-spin rounded-full h-10 w-10 border-b-2 border-mystic-900"></div>
</div>
}
else
{
<div class="card overflow-hidden animate-in fade-in slide-in-from-bottom-4 duration-700">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-mystic-50 border-b border-mystic-200">
<th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest">Book & Loan Info</th>
<th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest">Member Details</th>
<th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest">Loan Dates</th>
<th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest text-center">Status</th>
<th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest">Fines</th>
<th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-mystic-100">
@if (!_borrowings.Any())
{
<tr>
<td colspan="6" class="px-6 py-16 text-center text-mystic-400 italic bg-white">
No borrowing records found in the system.
</td>
</tr>
}
@foreach (var loan in PaginatedBorrowings)
{
<tr class="hover:bg-mystic-50/80 transition-colors">
<td class="px-6 py-5">
<div class="flex items-center gap-4">
<div class="w-10 h-14 bg-mystic-900/5 rounded-lg flex items-center justify-center text-mystic-400 border border-mystic-100">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18 18.246 18.477 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
</div>
<div class="min-w-0">
<p class="text-sm font-bold text-mystic-900 truncate mb-0.5">@loan.BookTitle</p>
<p class="text-[10px] font-mono text-mystic-400 uppercase tracking-tight">ID: #@loan.Id</p>
</div>
</div>
</td>
<td class="px-6 py-5">
<div class="flex flex-col gap-1">
<p class="text-sm font-semibold text-mystic-800">@loan.UserEmail</p>
<p class="text-[10px] text-mystic-500 font-medium tracking-wide">UID: #@loan.UserId</p>
</div>
</td>
<td class="px-6 py-5">
<div class="grid gap-1">
<div class="flex items-center gap-2 text-[11px]">
<span class="text-mystic-400 w-12">Out:</span>
<span class="font-bold text-mystic-700">@loan.BorrowDate.ToString("MMM dd, yyyy")</span>
</div>
<div class="flex items-center gap-2 text-[11px]">
<span class="text-mystic-400 w-12">Due:</span>
@{
var isOverdue = loan.DueDate < DateTime.Now && loan.Status == "Borrowed";
}
<span class="font-bold @(isOverdue ? "text-rose-600 animate-pulse" : "text-amber-700")">@loan.DueDate.ToString("MMM dd, yyyy")</span>
</div>
@if (loan.ReturnDate.HasValue)
{
<div class="flex items-center gap-2 text-[11px]">
<span class="text-mystic-400 w-12">Back:</span>
<span class="font-bold text-emerald-600">@loan.ReturnDate.Value.ToString("MMM dd, yyyy")</span>
</div>
}
</div>
</td>
<td class="px-6 py-5 text-center">
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider
@(loan.Status == "Borrowed" ? "bg-amber-100 text-amber-700" :
loan.Status == "PendingReturn" ? "bg-blue-600 text-white shadow-sm" :
"bg-emerald-100 text-emerald-700")">
@(loan.Status == "PendingReturn" ? "Pending Approval" : loan.Status)
</span>
</td>
<td class="px-6 py-5">
@if (loan.FineAmount > 0)
{
<div class="flex flex-col">
<span class="text-sm font-extrabold @(loan.IsFinePaid ? "text-mystic-400 line-through" : "text-rose-600")">
MMK @loan.FineAmount.ToString("N0")
</span>
@if (loan.IsFinePaid)
{
<span class="text-[9px] text-emerald-600 font-bold uppercase mt-0.5 tracking-tighter">Settled</span>
}
else
{
<span class="text-[9px] text-rose-400 font-bold uppercase mt-0.5 tracking-tighter">Pending</span>
}
</div>
}
else
{
<span class="text-[10px] text-mystic-300 font-bold italic">No Fines</span>
}
</td>
<td class="px-6 py-5 text-right">
@if (loan.Status == "PendingReturn")
{
<button @onclick="() => ApproveReturnAsync(loan)"
class="inline-flex items-center gap-2 bg-emerald-600 text-white px-4 py-2 rounded-lg text-xs font-bold hover:bg-emerald-700 transition-all shadow-md active:scale-95">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg>
Approve Return
</button>
}
else if (loan.Status == "Borrowed")
{
<button @onclick="() => ForceReturnAsync(loan)"
class="inline-flex items-center gap-2 bg-mystic-900 text-white px-4 py-2 rounded-lg text-xs font-bold hover:bg-mystic-800 transition-all shadow-sm active:scale-95">
Process Return
</button>
}
else
{
<div class="text-emerald-600">
<svg class="w-6 h-6 ml-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
}
</td>
</tr>
}
</tbody>
</table>
</div>
@if (TotalPages > 1)
{
<div class="p-6 bg-mystic-900/5 border-t border-mystic-200 flex items-center justify-between">
<p class="text-xs text-mystic-500 font-medium">
Showing <span class="font-bold text-mystic-900">@((_currentPage - 1) * _pageSize + 1)</span> to
<span class="font-bold text-mystic-900">@(Math.Min(_currentPage * _pageSize, _borrowings.Count()))</span> of
<span class="font-bold text-mystic-900">@_borrowings.Count()</span> records
</p>
<div class="flex items-center gap-2">
<button disabled="@(_currentPage == 1)" @onclick="() => _currentPage--"
class="p-2 rounded-lg border border-mystic-200 text-mystic-600 hover:bg-white disabled:opacity-30 disabled:cursor-not-allowed transition-all">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
</button>
<span class="text-xs font-bold text-mystic-900 px-2 tracking-tight">Page @_currentPage of @TotalPages</span>
<button disabled="@(_currentPage == TotalPages)" @onclick="() => _currentPage++"
class="p-2 rounded-lg border border-mystic-200 text-mystic-600 hover:bg-white disabled:opacity-30 disabled:cursor-not-allowed transition-all">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</button>
</div>
</div>
}
</div>
}
@code {
private bool _isLoading = true;
private IEnumerable<BorrowingDto> _borrowings = Enumerable.Empty<BorrowingDto>();
// Pagination
private int _currentPage = 1;
private int _pageSize = 10;
private IEnumerable<BorrowingDto> PaginatedBorrowings => _borrowings
.OrderByDescending(l => l.BorrowDate)
.Skip((_currentPage - 1) * _pageSize)
.Take(_pageSize);
private int TotalPages => (int)Math.Ceiling(_borrowings.Count() / (double)_pageSize);
protected override async Task OnInitializedAsync()
{
await LoadDataAsync();
}
private async Task LoadDataAsync()
{
_isLoading = true;
try
{
_borrowings = await ApiClient.GetAllBorrowingsAsync();
_currentPage = 1;
}
catch (Exception ex)
{
Console.WriteLine($"Error loading all borrowings: {ex.Message}");
}
finally
{
_isLoading = false;
}
}
private async Task ApproveReturnAsync(BorrowingDto loan)
{
bool confirmed = await JS.InvokeAsync<bool>("confirm", $"Approve return for \"{loan.BookTitle}\" borrowed by {loan.UserEmail}?");
if (!confirmed) return;
var result = await ApiClient.ReturnBookAsync(loan.Id);
if (result != null)
{
await JS.InvokeVoidAsync("showToast", "Return approved successfully!", "success");
await LoadDataAsync();
}
else
{
await JS.InvokeVoidAsync("showToast", "Failed to approve return.", "error");
}
}
private async Task ForceReturnAsync(BorrowingDto loan)
{
bool confirmed = await JS.InvokeAsync<bool>("confirm", $"Force process return for \"{loan.BookTitle}\" (Member: {loan.UserEmail})? Use this if the member returned the book in person.");
if (!confirmed) return;
var result = await ApiClient.ReturnBookAsync(loan.Id);
if (result != null)
{
await JS.InvokeVoidAsync("showToast", "Book marked as returned!", "success");
await LoadDataAsync();
}
else
{
await JS.InvokeVoidAsync("showToast", "Failed to process return.", "error");
}
}
}
|