Spaces:
Sleeping
Sleeping
Yuyuqt commited on
Commit ·
1723ad3
1
Parent(s): c312181
add: some features
Browse files- Backend/Features/Borrowing/BorrowingController.cs +15 -0
- Backend/Features/Borrowing/BorrowingService.cs +16 -0
- Frontend/Controllers/BorrowingsController.cs +22 -0
- Frontend/Models/Dtos/LibraryDtos.cs +21 -0
- Frontend/Services/LibraryApiClient.cs +21 -0
- Frontend/Views/Books/Details.cshtml +21 -15
- Frontend/Views/Books/Index.cshtml +15 -9
- Frontend/Views/Borrowings/Index.cshtml +13 -7
- Frontend/Views/Borrowings/Manage.cshtml +19 -10
- Frontend/Views/Borrowings/Rewards.cshtml +85 -0
- Frontend/Views/Home/Index.cshtml +21 -16
Backend/Features/Borrowing/BorrowingController.cs
CHANGED
|
@@ -34,6 +34,21 @@ namespace Backend.Features.Borrowings
|
|
| 34 |
}
|
| 35 |
}
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
[HttpPost("return/{id}")]
|
| 38 |
[Authorize(Roles = "Librarian")]
|
| 39 |
public async Task<ActionResult<BorrowingDto>> ReturnBook(int id)
|
|
|
|
| 34 |
}
|
| 35 |
}
|
| 36 |
|
| 37 |
+
[HttpPost("return-request/{id}")]
|
| 38 |
+
[Authorize]
|
| 39 |
+
public async Task<ActionResult<BorrowingDto>> RequestReturn(int id)
|
| 40 |
+
{
|
| 41 |
+
try
|
| 42 |
+
{
|
| 43 |
+
var borrowing = await _borrowingService.RequestReturnAsync(id);
|
| 44 |
+
return Ok(borrowing);
|
| 45 |
+
}
|
| 46 |
+
catch (Exception ex)
|
| 47 |
+
{
|
| 48 |
+
return BadRequest(new { message = ex.Message });
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
[HttpPost("return/{id}")]
|
| 53 |
[Authorize(Roles = "Librarian")]
|
| 54 |
public async Task<ActionResult<BorrowingDto>> ReturnBook(int id)
|
Backend/Features/Borrowing/BorrowingService.cs
CHANGED
|
@@ -7,6 +7,7 @@ namespace Backend.Features.Borrowings
|
|
| 7 |
public interface IBorrowingService
|
| 8 |
{
|
| 9 |
Task<BorrowingDto> BorrowBookAsync(int userId, int bookId);
|
|
|
|
| 10 |
Task<BorrowingDto> ReturnBookAsync(int borrowingId);
|
| 11 |
Task<IEnumerable<BorrowingDto>> GetUserBorrowingsAsync(int userId);
|
| 12 |
Task<IEnumerable<BorrowingDto>> GetAllBorrowingsAsync();
|
|
@@ -107,6 +108,21 @@ namespace Backend.Features.Borrowings
|
|
| 107 |
return MapToDto(borrowing);
|
| 108 |
}
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
public async Task<BorrowingDto> ReturnBookAsync(int borrowingId)
|
| 111 |
{
|
| 112 |
var borrowing = await _context.Borrowings
|
|
|
|
| 7 |
public interface IBorrowingService
|
| 8 |
{
|
| 9 |
Task<BorrowingDto> BorrowBookAsync(int userId, int bookId);
|
| 10 |
+
Task<BorrowingDto> RequestReturnAsync(int borrowingId);
|
| 11 |
Task<BorrowingDto> ReturnBookAsync(int borrowingId);
|
| 12 |
Task<IEnumerable<BorrowingDto>> GetUserBorrowingsAsync(int userId);
|
| 13 |
Task<IEnumerable<BorrowingDto>> GetAllBorrowingsAsync();
|
|
|
|
| 108 |
return MapToDto(borrowing);
|
| 109 |
}
|
| 110 |
|
| 111 |
+
public async Task<BorrowingDto> RequestReturnAsync(int borrowingId)
|
| 112 |
+
{
|
| 113 |
+
var borrowing = await _context.Borrowings
|
| 114 |
+
.Include(b => b.Book)
|
| 115 |
+
.Include(b => b.User)
|
| 116 |
+
.FirstOrDefaultAsync(b => b.Id == borrowingId);
|
| 117 |
+
|
| 118 |
+
if (borrowing == null) throw new Exception("Borrowing record not found.");
|
| 119 |
+
if (borrowing.Status != "Borrowed") throw new Exception("Only borrowed books can be requested for return.");
|
| 120 |
+
|
| 121 |
+
borrowing.Status = "PendingReturn";
|
| 122 |
+
await _context.SaveChangesAsync();
|
| 123 |
+
return MapToDto(borrowing);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
public async Task<BorrowingDto> ReturnBookAsync(int borrowingId)
|
| 127 |
{
|
| 128 |
var borrowing = await _context.Borrowings
|
Frontend/Controllers/BorrowingsController.cs
CHANGED
|
@@ -20,6 +20,28 @@ namespace Frontend.Controllers
|
|
| 20 |
return View(borrowings);
|
| 21 |
}
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
[Authorize(Roles = "Librarian")]
|
| 24 |
public async Task<IActionResult> Manage()
|
| 25 |
{
|
|
|
|
| 20 |
return View(borrowings);
|
| 21 |
}
|
| 22 |
|
| 23 |
+
[HttpPost]
|
| 24 |
+
public async Task<IActionResult> RequestReturn(int id)
|
| 25 |
+
{
|
| 26 |
+
var success = await _apiClient.RequestReturnAsync(id);
|
| 27 |
+
if (success)
|
| 28 |
+
{
|
| 29 |
+
TempData["Success"] = "Return request sent! Awaiting librarian approval.";
|
| 30 |
+
}
|
| 31 |
+
else
|
| 32 |
+
{
|
| 33 |
+
TempData["Error"] = "Failed to request return.";
|
| 34 |
+
}
|
| 35 |
+
return RedirectToAction(nameof(Index));
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
[HttpGet]
|
| 39 |
+
public async Task<IActionResult> Rewards()
|
| 40 |
+
{
|
| 41 |
+
var rewards = await _apiClient.GetActiveRewardsAsync();
|
| 42 |
+
return View(rewards);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
[Authorize(Roles = "Librarian")]
|
| 46 |
public async Task<IActionResult> Manage()
|
| 47 |
{
|
Frontend/Models/Dtos/LibraryDtos.cs
CHANGED
|
@@ -145,6 +145,27 @@ namespace Frontend.Models.Dtos
|
|
| 145 |
public double LifetimePoints { get; set; }
|
| 146 |
}
|
| 147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
// User DTOs
|
| 149 |
public class UserDto
|
| 150 |
{
|
|
|
|
| 145 |
public double LifetimePoints { get; set; }
|
| 146 |
}
|
| 147 |
|
| 148 |
+
public class LoyaltyRewardDto
|
| 149 |
+
{
|
| 150 |
+
[JsonPropertyName("id")]
|
| 151 |
+
public string Id { get; set; } = string.Empty;
|
| 152 |
+
|
| 153 |
+
[JsonPropertyName("name")]
|
| 154 |
+
public string Name { get; set; } = string.Empty;
|
| 155 |
+
|
| 156 |
+
[JsonPropertyName("description")]
|
| 157 |
+
public string Description { get; set; } = string.Empty;
|
| 158 |
+
|
| 159 |
+
[JsonPropertyName("pointCost")]
|
| 160 |
+
public double PointCost { get; set; }
|
| 161 |
+
|
| 162 |
+
[JsonPropertyName("stockQuantity")]
|
| 163 |
+
public int StockQuantity { get; set; }
|
| 164 |
+
|
| 165 |
+
[JsonPropertyName("isActive")]
|
| 166 |
+
public bool IsActive { get; set; }
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
// User DTOs
|
| 170 |
public class UserDto
|
| 171 |
{
|
Frontend/Services/LibraryApiClient.cs
CHANGED
|
@@ -183,6 +183,27 @@ namespace Frontend.Services
|
|
| 183 |
}
|
| 184 |
}
|
| 185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
#region Users
|
| 187 |
public async Task<IEnumerable<UserDto>> GetUsersAsync()
|
| 188 |
{
|
|
|
|
| 183 |
}
|
| 184 |
}
|
| 185 |
|
| 186 |
+
public async Task<bool> RequestReturnAsync(int borrowingId)
|
| 187 |
+
{
|
| 188 |
+
var response = await _httpClient.PostAsync($"api/borrowings/return-request/{borrowingId}", null);
|
| 189 |
+
return response.IsSuccessStatusCode;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
public async Task<IEnumerable<LoyaltyRewardDto>> GetActiveRewardsAsync()
|
| 193 |
+
{
|
| 194 |
+
try {
|
| 195 |
+
// Fetch from the external loyalty API directly or via our backend proxy
|
| 196 |
+
// The user specified the URL: http://150.95.88.91:4100/api/v1/rewards/active/THS-LMS
|
| 197 |
+
// We'll use a new HttpClient or just the existing one if configured correctly.
|
| 198 |
+
// However, our backend doesn't have a proxy for this yet.
|
| 199 |
+
// For simplicity, let's assume we call it directly or via a backend endpoint we'll add.
|
| 200 |
+
// Better: Let's add a backend proxy in LoyaltyController to avoid CORS issues if this was a browser app.
|
| 201 |
+
// In ASP.NET Core MVC, the server-side HttpClient can call it directly.
|
| 202 |
+
var client = new HttpClient();
|
| 203 |
+
return await client.GetFromJsonAsync<IEnumerable<LoyaltyRewardDto>>("http://150.95.88.91:4100/api/v1/rewards/active/THS-LMS") ?? Enumerable.Empty<LoyaltyRewardDto>();
|
| 204 |
+
} catch { return Enumerable.Empty<LoyaltyRewardDto>(); }
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
#region Users
|
| 208 |
public async Task<IEnumerable<UserDto>> GetUsersAsync()
|
| 209 |
{
|
Frontend/Views/Books/Details.cshtml
CHANGED
|
@@ -46,25 +46,31 @@
|
|
| 46 |
</div>
|
| 47 |
|
| 48 |
<div class="flex items-center gap-4 pt-4">
|
| 49 |
-
@if (
|
| 50 |
{
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
<
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
}
|
| 56 |
-
|
|
|
|
| 57 |
{
|
| 58 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
-
|
| 61 |
-
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn-secondary py-3 px-8 text-base">Edit Details</a>
|
| 62 |
-
|
| 63 |
-
<form asp-action="Delete" asp-route-id="@Model.Id" method="POST" onsubmit="return confirm('Are you sure you want to remove this book from the catalog?');">
|
| 64 |
-
<button type="submit" class="p-3 text-rose-600 hover:text-rose-700 hover:bg-rose-50 rounded-xl transition-colors">
|
| 65 |
-
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
| 66 |
-
</button>
|
| 67 |
-
</form>
|
| 68 |
</div>
|
| 69 |
</div>
|
| 70 |
</div>
|
|
|
|
| 46 |
</div>
|
| 47 |
|
| 48 |
<div class="flex items-center gap-4 pt-4">
|
| 49 |
+
@if (User.IsInRole("Member") || !User.Identity.IsAuthenticated)
|
| 50 |
{
|
| 51 |
+
@if (Model.AvailableCopies > 0)
|
| 52 |
+
{
|
| 53 |
+
<form asp-controller="Borrowings" asp-action="Borrow" method="POST">
|
| 54 |
+
<input type="hidden" name="BookId" value="@Model.Id" />
|
| 55 |
+
<button type="submit" class="btn-primary py-3 px-8 text-base shadow-lg shadow-slate-200">Borrow This Book</button>
|
| 56 |
+
</form>
|
| 57 |
+
}
|
| 58 |
+
else
|
| 59 |
+
{
|
| 60 |
+
<button disabled class="btn-primary py-3 px-8 text-base opacity-50 cursor-not-allowed">Out of Stock</button>
|
| 61 |
+
}
|
| 62 |
}
|
| 63 |
+
|
| 64 |
+
@if (User.IsInRole("Librarian"))
|
| 65 |
{
|
| 66 |
+
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn-secondary py-3 px-8 text-base">Edit Details</a>
|
| 67 |
+
|
| 68 |
+
<form asp-action="Delete" asp-route-id="@Model.Id" method="POST" onsubmit="return confirm('Are you sure you want to remove this book from the catalog?');">
|
| 69 |
+
<button type="submit" class="p-3 text-rose-600 hover:text-rose-700 hover:bg-rose-50 rounded-xl transition-colors">
|
| 70 |
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
| 71 |
+
</button>
|
| 72 |
+
</form>
|
| 73 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
</div>
|
| 75 |
</div>
|
| 76 |
</div>
|
Frontend/Views/Books/Index.cshtml
CHANGED
|
@@ -8,12 +8,15 @@
|
|
| 8 |
<h1 class="text-3xl font-bold tracking-tight text-slate-900">Books Catalog</h1>
|
| 9 |
<p class="mt-2 text-slate-500">Explore and discover your next great read.</p>
|
| 10 |
</div>
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
| 17 |
</div>
|
| 18 |
|
| 19 |
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-10">
|
|
@@ -64,9 +67,12 @@
|
|
| 64 |
</div>
|
| 65 |
<div class="mt-4 pt-4 border-t border-slate-50 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
| 66 |
<a asp-action="Details" asp-route-id="@book.Id" class="flex-1 btn-secondary py-1 text-[11px]">Details</a>
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
| 70 |
</div>
|
| 71 |
</div>
|
| 72 |
</div>
|
|
|
|
| 8 |
<h1 class="text-3xl font-bold tracking-tight text-slate-900">Books Catalog</h1>
|
| 9 |
<p class="mt-2 text-slate-500">Explore and discover your next great read.</p>
|
| 10 |
</div>
|
| 11 |
+
@if (User.IsInRole("Librarian"))
|
| 12 |
+
{
|
| 13 |
+
<div class="flex items-center gap-3">
|
| 14 |
+
<a asp-action="Create" class="btn-primary">
|
| 15 |
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
| 16 |
+
Add New Book
|
| 17 |
+
</a>
|
| 18 |
+
</div>
|
| 19 |
+
}
|
| 20 |
</div>
|
| 21 |
|
| 22 |
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-10">
|
|
|
|
| 67 |
</div>
|
| 68 |
<div class="mt-4 pt-4 border-t border-slate-50 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
| 69 |
<a asp-action="Details" asp-route-id="@book.Id" class="flex-1 btn-secondary py-1 text-[11px]">Details</a>
|
| 70 |
+
@if (User.IsInRole("Librarian"))
|
| 71 |
+
{
|
| 72 |
+
<a asp-action="Edit" asp-route-id="@book.Id" class="p-1.5 rounded-md border border-slate-200 hover:bg-slate-50 text-slate-600">
|
| 73 |
+
<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="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
|
| 74 |
+
</a>
|
| 75 |
+
}
|
| 76 |
</div>
|
| 77 |
</div>
|
| 78 |
</div>
|
Frontend/Views/Borrowings/Index.cshtml
CHANGED
|
@@ -14,7 +14,7 @@
|
|
| 14 |
<thead>
|
| 15 |
<tr class="bg-slate-50/50 border-b border-slate-100">
|
| 16 |
<th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Book Information</th>
|
| 17 |
-
@if (User.IsInRole("
|
| 18 |
{
|
| 19 |
<th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Member</th>
|
| 20 |
}
|
|
@@ -28,7 +28,7 @@
|
|
| 28 |
@if (!Model.Any())
|
| 29 |
{
|
| 30 |
<tr>
|
| 31 |
-
<td colspan="@(User.IsInRole("
|
| 32 |
You don't have any borrowing history yet.
|
| 33 |
</td>
|
| 34 |
</tr>
|
|
@@ -47,7 +47,7 @@
|
|
| 47 |
</div>
|
| 48 |
</div>
|
| 49 |
</td>
|
| 50 |
-
@if (User.IsInRole("
|
| 51 |
{
|
| 52 |
<td class="px-6 py-4">
|
| 53 |
<div class="text-sm font-medium text-slate-900 truncate">@loan.UserEmail</div>
|
|
@@ -66,8 +66,10 @@
|
|
| 66 |
</td>
|
| 67 |
<td class="px-6 py-4">
|
| 68 |
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
| 69 |
-
@(loan.Status == "Borrowed" ? "bg-amber-100 text-amber-700" :
|
| 70 |
-
|
|
|
|
|
|
|
| 71 |
</span>
|
| 72 |
</td>
|
| 73 |
<td class="px-6 py-4">
|
|
@@ -91,11 +93,15 @@
|
|
| 91 |
<td class="px-6 py-4 text-right">
|
| 92 |
@if (loan.Status == "Borrowed")
|
| 93 |
{
|
| 94 |
-
<form asp-action="
|
| 95 |
<input type="hidden" name="id" value="@loan.Id" />
|
| 96 |
-
<button type="submit" class="btn-secondary py-1.5 px-4 text-xs font-bold">
|
| 97 |
</form>
|
| 98 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
</td>
|
| 100 |
</tr>
|
| 101 |
}
|
|
|
|
| 14 |
<thead>
|
| 15 |
<tr class="bg-slate-50/50 border-b border-slate-100">
|
| 16 |
<th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Book Information</th>
|
| 17 |
+
@if (User.IsInRole("Librarian"))
|
| 18 |
{
|
| 19 |
<th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Member</th>
|
| 20 |
}
|
|
|
|
| 28 |
@if (!Model.Any())
|
| 29 |
{
|
| 30 |
<tr>
|
| 31 |
+
<td colspan="@(User.IsInRole("Librarian") ? 6 : 5)" class="px-6 py-12 text-center text-slate-400 italic">
|
| 32 |
You don't have any borrowing history yet.
|
| 33 |
</td>
|
| 34 |
</tr>
|
|
|
|
| 47 |
</div>
|
| 48 |
</div>
|
| 49 |
</td>
|
| 50 |
+
@if (User.IsInRole("Librarian"))
|
| 51 |
{
|
| 52 |
<td class="px-6 py-4">
|
| 53 |
<div class="text-sm font-medium text-slate-900 truncate">@loan.UserEmail</div>
|
|
|
|
| 66 |
</td>
|
| 67 |
<td class="px-6 py-4">
|
| 68 |
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
| 69 |
+
@(loan.Status == "Borrowed" ? "bg-amber-100 text-amber-700" :
|
| 70 |
+
loan.Status == "PendingReturn" ? "bg-blue-100 text-blue-700" :
|
| 71 |
+
"bg-emerald-100 text-emerald-700")">
|
| 72 |
+
@(loan.Status == "PendingReturn" ? "Pending Approval" : loan.Status)
|
| 73 |
</span>
|
| 74 |
</td>
|
| 75 |
<td class="px-6 py-4">
|
|
|
|
| 93 |
<td class="px-6 py-4 text-right">
|
| 94 |
@if (loan.Status == "Borrowed")
|
| 95 |
{
|
| 96 |
+
<form asp-action="RequestReturn" method="POST" onsubmit="return confirm('Request return for this book? It will be marked as pending until approved.');">
|
| 97 |
<input type="hidden" name="id" value="@loan.Id" />
|
| 98 |
+
<button type="submit" class="btn-secondary py-1.5 px-4 text-xs font-bold">Return Book</button>
|
| 99 |
</form>
|
| 100 |
}
|
| 101 |
+
else if (loan.Status == "PendingReturn")
|
| 102 |
+
{
|
| 103 |
+
<span class="text-xs font-medium text-slate-400">Awaiting Approval</span>
|
| 104 |
+
}
|
| 105 |
</td>
|
| 106 |
</tr>
|
| 107 |
}
|
Frontend/Views/Borrowings/Manage.cshtml
CHANGED
|
@@ -84,8 +84,10 @@
|
|
| 84 |
</td>
|
| 85 |
<td class="px-6 py-5 text-center">
|
| 86 |
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider
|
| 87 |
-
@(loan.Status == "Borrowed" ? "bg-
|
| 88 |
-
|
|
|
|
|
|
|
| 89 |
</span>
|
| 90 |
</td>
|
| 91 |
<td class="px-6 py-5">
|
|
@@ -111,24 +113,31 @@
|
|
| 111 |
}
|
| 112 |
</td>
|
| 113 |
<td class="px-6 py-5 text-right">
|
| 114 |
-
@if (loan.Status == "
|
| 115 |
{
|
| 116 |
-
<form asp-action="Return" method="POST" onsubmit="return confirm('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
<input type="hidden" name="id" value="@loan.Id" />
|
| 118 |
<input type="hidden" name="fromManage" value="true" />
|
| 119 |
<button type="submit" class="inline-flex items-center gap-2 bg-slate-900 text-white px-4 py-2 rounded-lg text-xs font-bold hover:bg-slate-800 transition-all shadow-sm">
|
| 120 |
-
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M11 19l-7-7 7-7m8 14l-7-7 7-7"></path></svg>
|
| 121 |
Process Return
|
| 122 |
</button>
|
| 123 |
</form>
|
| 124 |
}
|
| 125 |
else
|
| 126 |
{
|
| 127 |
-
<div class="group relative inline-block">
|
| 128 |
-
<
|
| 129 |
-
<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="M5 13l4 4L19 7"></path></svg>
|
| 130 |
-
</div>
|
| 131 |
-
<span class="absolute hidden group-hover:block bg-slate-800 text-white text-[9px] px-2 py-1 rounded right-0 top-full mt-1 whitespace-nowrap z-10">Completed</span>
|
| 132 |
</div>
|
| 133 |
}
|
| 134 |
</td>
|
|
|
|
| 84 |
</td>
|
| 85 |
<td class="px-6 py-5 text-center">
|
| 86 |
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider
|
| 87 |
+
@(loan.Status == "Borrowed" ? "bg-amber-100 text-amber-700" :
|
| 88 |
+
loan.Status == "PendingReturn" ? "bg-blue-600 text-white shadow-sm" :
|
| 89 |
+
"bg-emerald-100 text-emerald-700")">
|
| 90 |
+
@(loan.Status == "PendingReturn" ? "Pending Approval" : loan.Status)
|
| 91 |
</span>
|
| 92 |
</td>
|
| 93 |
<td class="px-6 py-5">
|
|
|
|
| 113 |
}
|
| 114 |
</td>
|
| 115 |
<td class="px-6 py-5 text-right">
|
| 116 |
+
@if (loan.Status == "PendingReturn")
|
| 117 |
{
|
| 118 |
+
<form asp-action="Return" method="POST" onsubmit="return confirm('Approve this return?');">
|
| 119 |
+
<input type="hidden" name="id" value="@loan.Id" />
|
| 120 |
+
<input type="hidden" name="fromManage" value="true" />
|
| 121 |
+
<button type="submit" 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">
|
| 122 |
+
<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>
|
| 123 |
+
Approve Return
|
| 124 |
+
</button>
|
| 125 |
+
</form>
|
| 126 |
+
}
|
| 127 |
+
else if (loan.Status == "Borrowed")
|
| 128 |
+
{
|
| 129 |
+
<form asp-action="Return" method="POST" onsubmit="return confirm('Force process return for this book?');">
|
| 130 |
<input type="hidden" name="id" value="@loan.Id" />
|
| 131 |
<input type="hidden" name="fromManage" value="true" />
|
| 132 |
<button type="submit" class="inline-flex items-center gap-2 bg-slate-900 text-white px-4 py-2 rounded-lg text-xs font-bold hover:bg-slate-800 transition-all shadow-sm">
|
|
|
|
| 133 |
Process Return
|
| 134 |
</button>
|
| 135 |
</form>
|
| 136 |
}
|
| 137 |
else
|
| 138 |
{
|
| 139 |
+
<div class="group relative inline-block text-emerald-600">
|
| 140 |
+
<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>
|
|
|
|
|
|
|
|
|
|
| 141 |
</div>
|
| 142 |
}
|
| 143 |
</td>
|
Frontend/Views/Borrowings/Rewards.cshtml
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@model IEnumerable<Frontend.Models.Dtos.LoyaltyRewardDto>
|
| 2 |
+
@{
|
| 3 |
+
ViewData["Title"] = "Loyalty Rewards";
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
<div class="mb-10">
|
| 7 |
+
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
| 8 |
+
<div>
|
| 9 |
+
<h1 class="text-3xl font-extrabold tracking-tight text-slate-900 border-b border-slate-100 pb-4 mb-2">Loyalty Rewards</h1>
|
| 10 |
+
<p class="text-slate-500 font-medium">Redeem your hard-earned points for exclusive library benefits.</p>
|
| 11 |
+
</div>
|
| 12 |
+
<div class="flex items-center gap-4 bg-amber-50 border border-amber-100 px-6 py-4 rounded-2xl shadow-sm">
|
| 13 |
+
<div class="p-2 bg-amber-100 rounded-lg text-amber-600">
|
| 14 |
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"></path></svg>
|
| 15 |
+
</div>
|
| 16 |
+
<div>
|
| 17 |
+
<p class="text-[10px] font-bold text-amber-600 uppercase tracking-widest mb-0.5">Your Balance</p>
|
| 18 |
+
@* This would ideally be fetched from a global layout or specific viewbag *@
|
| 19 |
+
<p class="text-2xl font-black text-slate-900">Points</p>
|
| 20 |
+
</div>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
|
| 25 |
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
| 26 |
+
@if (!Model.Any())
|
| 27 |
+
{
|
| 28 |
+
<div class="col-span-full py-20 text-center bg-slate-50 rounded-3xl border-2 border-dashed border-slate-200">
|
| 29 |
+
<div class="text-slate-400 mb-4">
|
| 30 |
+
<svg class="w-16 h-16 mx-auto opacity-20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path></svg>
|
| 31 |
+
</div>
|
| 32 |
+
<p class="text-slate-500 font-medium italic">No active rewards available at the moment. Check back soon!</p>
|
| 33 |
+
</div>
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
@foreach (var reward in Model)
|
| 37 |
+
{
|
| 38 |
+
<div class="group relative card p-6 hover:shadow-2xl hover:-translate-y-1 transition-all duration-300 border-t-4 border-amber-400">
|
| 39 |
+
<div class="flex justify-between items-start mb-6">
|
| 40 |
+
<div class="p-3 bg-amber-50 rounded-xl text-amber-600 group-hover:bg-amber-400 group-hover:text-white transition-colors">
|
| 41 |
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"></path></svg>
|
| 42 |
+
</div>
|
| 43 |
+
<div class="text-right">
|
| 44 |
+
<span class="text-3xl font-black text-slate-900">@reward.PointCost</span>
|
| 45 |
+
<span class="text-[10px] block font-bold text-slate-400 uppercase tracking-tighter">Points Required</span>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
<h3 class="text-lg font-bold text-slate-900 mb-2 truncate">@reward.Name</h3>
|
| 50 |
+
<p class="text-sm text-slate-500 leading-relaxed mb-6 h-12 overflow-hidden line-clamp-2">@reward.Description</p>
|
| 51 |
+
|
| 52 |
+
<div class="flex items-center justify-between pt-6 border-t border-slate-50">
|
| 53 |
+
<div class="flex flex-col">
|
| 54 |
+
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Stock</span>
|
| 55 |
+
<span class="text-sm font-bold text-slate-700">@reward.StockQuantity available</span>
|
| 56 |
+
</div>
|
| 57 |
+
<button class="bg-slate-900 text-white px-5 py-2.5 rounded-xl text-xs font-bold hover:bg-slate-800 hover:shadow-lg transition-all active:scale-95">
|
| 58 |
+
Redeem Reward
|
| 59 |
+
</button>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
@if (reward.StockQuantity < 10)
|
| 63 |
+
{
|
| 64 |
+
<div class="absolute top-2 right-2 flex">
|
| 65 |
+
<span class="animate-pulse bg-rose-500 text-white text-[8px] font-black px-1.5 py-0.5 rounded uppercase">Limited</span>
|
| 66 |
+
</div>
|
| 67 |
+
}
|
| 68 |
+
</div>
|
| 69 |
+
}
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<div class="mt-12 p-8 bg-slate-900 rounded-3xl text-white relative overflow-hidden">
|
| 73 |
+
<div class="relative z-10 flex flex-col md:flex-row items-center justify-between gap-8">
|
| 74 |
+
<div class="text-center md:text-left">
|
| 75 |
+
<h2 class="text-2xl font-bold mb-2">How to earn more points?</h2>
|
| 76 |
+
<p class="text-slate-400 max-w-md">Borrow books, subscribe to memberships, and return books on time to stack up your points and unlock even bigger rewards!</p>
|
| 77 |
+
</div>
|
| 78 |
+
<a asp-controller="Books" asp-action="Index" class="bg-amber-400 text-slate-900 px-8 py-4 rounded-2xl font-black hover:bg-amber-300 transition-all shadow-xl shadow-amber-400/20 whitespace-nowrap">
|
| 79 |
+
Browse Books Now
|
| 80 |
+
</a>
|
| 81 |
+
</div>
|
| 82 |
+
<!-- Decorative background element -->
|
| 83 |
+
<div class="absolute -right-10 -bottom-10 w-64 h-64 bg-amber-400/10 rounded-full blur-3xl"></div>
|
| 84 |
+
<div class="absolute -left-10 -top-10 w-48 h-48 bg-blue-400/5 rounded-full blur-3xl"></div>
|
| 85 |
+
</div>
|
Frontend/Views/Home/Index.cshtml
CHANGED
|
@@ -10,26 +10,31 @@
|
|
| 10 |
@if (User.Identity?.IsAuthenticated == true)
|
| 11 |
{
|
| 12 |
<!-- Loyalty Banner -->
|
| 13 |
-
<
|
| 14 |
-
<div class="
|
| 15 |
-
<div>
|
| 16 |
-
<
|
| 17 |
-
|
| 18 |
-
<
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
| 20 |
</div>
|
| 21 |
-
<
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
<
|
| 26 |
-
<
|
|
|
|
|
|
|
| 27 |
</div>
|
| 28 |
</div>
|
|
|
|
|
|
|
| 29 |
</div>
|
| 30 |
-
|
| 31 |
-
<div class="absolute -right-10 -top-10 w-48 h-48 bg-white/10 rounded-full blur-2xl transform group-hover:scale-110 transition-transform duration-700"></div>
|
| 32 |
-
</div>
|
| 33 |
}
|
| 34 |
|
| 35 |
<!-- Stats Grid -->
|
|
|
|
| 10 |
@if (User.Identity?.IsAuthenticated == true)
|
| 11 |
{
|
| 12 |
<!-- Loyalty Banner -->
|
| 13 |
+
<a asp-controller="Borrowings" asp-action="Rewards" class="block group">
|
| 14 |
+
<div class="bg-gradient-to-r from-amber-500 to-amber-700 rounded-2xl p-6 mb-10 text-white shadow-lg relative overflow-hidden transition-all duration-300 hover:shadow-xl hover:translate-y-[-2px] border border-white/10">
|
| 15 |
+
<div class="relative z-10 flex items-center justify-between">
|
| 16 |
+
<div>
|
| 17 |
+
<p class="text-amber-100 text-sm font-bold uppercase tracking-widest mb-1">Your Loyalty Status</p>
|
| 18 |
+
<div class="flex items-baseline gap-3">
|
| 19 |
+
<h2 class="text-4xl font-extrabold">@ViewBag.LoyaltyPoints</h2>
|
| 20 |
+
<span class="text-amber-200 font-medium tracking-wide">Points</span>
|
| 21 |
+
</div>
|
| 22 |
+
<p class="text-[10px] text-amber-300/60 mt-1 uppercase tracking-tighter">Connection: @ViewBag.LoyaltyStatus</p>
|
| 23 |
</div>
|
| 24 |
+
<div class="text-right flex items-center gap-4">
|
| 25 |
+
<div class="inline-flex items-center gap-2 bg-black/20 backdrop-blur-sm px-4 py-2 rounded-full border border-white/10 shadow-inner">
|
| 26 |
+
<svg class="w-5 h-5 text-amber-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"></path></svg>
|
| 27 |
+
<span class="font-bold text-lg">@ViewBag.LoyaltyTier</span>
|
| 28 |
+
</div>
|
| 29 |
+
<div class="p-2 bg-white/20 rounded-full group-hover:bg-white/30 transition-colors">
|
| 30 |
+
<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="M9 5l7 7-7 7"></path></svg>
|
| 31 |
+
</div>
|
| 32 |
</div>
|
| 33 |
</div>
|
| 34 |
+
<!-- Decorative Background -->
|
| 35 |
+
<div class="absolute -right-10 -top-10 w-48 h-48 bg-white/10 rounded-full blur-2xl transform group-hover:scale-110 transition-transform duration-700"></div>
|
| 36 |
</div>
|
| 37 |
+
</a>
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
<!-- Stats Grid -->
|