Spaces:
Sleeping
Sleeping
Yuyuqt commited on
Commit ·
ccc2569
1
Parent(s): cf9e5bb
add: removed firebase json file
Browse files- .gitignore +4 -1
- Backend/Backend.csproj +5 -0
- Backend/Features/Books/BookController.cs +10 -0
- Backend/Features/Books/BookService.cs +10 -0
- Backend/Features/Borrowing/BorrowingService.cs +30 -1
- Backend/Features/Notification/INotificationService.cs +13 -0
- Backend/Features/Notification/NotificationBackgroundService.cs +113 -0
- Backend/Features/Notification/NotificationModels.cs +36 -0
- Backend/Features/Notification/NotificationService.cs +130 -0
- Backend/Features/Notification/NotificationsController.cs +101 -0
- Backend/Features/Users/UserController.cs +20 -0
- Backend/Features/Users/UserService.cs +13 -0
- Backend/Features/Wishlist/WishlistController.cs +47 -0
- Backend/Features/Wishlist/WishlistService.cs +57 -0
- Backend/Program.cs +13 -0
- BlazorFrontend/BlazorFrontend.csproj +1 -0
- BlazorFrontend/Components/App.razor +1 -0
- BlazorFrontend/Components/Layout/MainLayout.razor +33 -2
- BlazorFrontend/Components/Pages/FcmTest.razor +78 -0
- BlazorFrontend/Components/Pages/Notifications.razor +158 -0
- BlazorFrontend/Components/Pages/Wishlist.razor +142 -131
- BlazorFrontend/Models/Dtos/LibraryDtos.cs +12 -0
- BlazorFrontend/Services/LibraryApiClient.cs +40 -0
- BlazorFrontend/Services/WishlistService.cs +45 -3
- BlazorFrontend/wwwroot/firebase-init.js +50 -0
- BlazorFrontend/wwwroot/firebase-messaging-sw.js +37 -0
- DbConnect/Data/AppDbContext.cs +45 -4
- DbConnect/Entities/Notification.cs +26 -0
- DbConnect/Entities/User.cs +3 -1
- DbConnect/Entities/WishlistItem.cs +18 -0
- DbConnect/Migrations/20260427043736_AddNotifications.Designer.cs +469 -0
- DbConnect/Migrations/20260427043736_AddNotifications.cs +74 -0
- DbConnect/Migrations/20260427052139_UpdateUserWithFcmToken.Designer.cs +473 -0
- DbConnect/Migrations/20260427052139_UpdateUserWithFcmToken.cs +29 -0
- DbConnect/Migrations/20260427071940_AddWishlistItems.Designer.cs +522 -0
- DbConnect/Migrations/20260427071940_AddWishlistItems.cs +59 -0
- DbConnect/Migrations/AppDbContextModelSnapshot.cs +519 -0
.gitignore
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
## Ignore Visual Studio temporary files, build results, and
|
| 2 |
## files generated by popular Visual Studio add-ons.
|
| 3 |
##
|
|
@@ -360,4 +362,5 @@ MigrationBackup/
|
|
| 360 |
.ionide/
|
| 361 |
|
| 362 |
# Fody - auto-generated XML schema
|
| 363 |
-
FodyWeavers.xsd
|
|
|
|
|
|
| 1 |
+
./Backend/LibraryFirebase.json
|
| 2 |
+
|
| 3 |
## Ignore Visual Studio temporary files, build results, and
|
| 4 |
## files generated by popular Visual Studio add-ons.
|
| 5 |
##
|
|
|
|
| 362 |
.ionide/
|
| 363 |
|
| 364 |
# Fody - auto-generated XML schema
|
| 365 |
+
FodyWeavers.xsd
|
| 366 |
+
/Backend/LibraryFirebase.json
|
Backend/Backend.csproj
CHANGED
|
@@ -8,7 +8,12 @@
|
|
| 8 |
|
| 9 |
<ItemGroup>
|
| 10 |
<PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
|
|
|
|
| 11 |
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.13" />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
| 13 |
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.17.0" />
|
| 14 |
</ItemGroup>
|
|
|
|
| 8 |
|
| 9 |
<ItemGroup>
|
| 10 |
<PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
|
| 11 |
+
<PackageReference Include="FirebaseAdmin" Version="3.5.0" />
|
| 12 |
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.13" />
|
| 13 |
+
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
| 14 |
+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
| 15 |
+
<PrivateAssets>all</PrivateAssets>
|
| 16 |
+
</PackageReference>
|
| 17 |
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
| 18 |
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.17.0" />
|
| 19 |
</ItemGroup>
|
Backend/Features/Books/BookController.cs
CHANGED
|
@@ -31,6 +31,16 @@ namespace Backend.Features.Books
|
|
| 31 |
return Ok(book);
|
| 32 |
}
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
[HttpPost]
|
| 35 |
[Authorize(Roles = "Librarian")]
|
| 36 |
public async Task<ActionResult<BookDto>> CreateBook([FromBody] BookCreateRequest request)
|
|
|
|
| 31 |
return Ok(book);
|
| 32 |
}
|
| 33 |
|
| 34 |
+
[HttpGet("by-ids")]
|
| 35 |
+
[Authorize]
|
| 36 |
+
public async Task<ActionResult<IEnumerable<BookDto>>> GetBooksByIds([FromQuery] string ids)
|
| 37 |
+
{
|
| 38 |
+
if (string.IsNullOrEmpty(ids)) return Ok(new List<BookDto>());
|
| 39 |
+
var idList = ids.Split(',').Select(int.Parse).ToList();
|
| 40 |
+
var books = await _bookService.GetBooksByIdsAsync(idList);
|
| 41 |
+
return Ok(books);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
[HttpPost]
|
| 45 |
[Authorize(Roles = "Librarian")]
|
| 46 |
public async Task<ActionResult<BookDto>> CreateBook([FromBody] BookCreateRequest request)
|
Backend/Features/Books/BookService.cs
CHANGED
|
@@ -9,6 +9,7 @@ namespace Backend.Features.Books
|
|
| 9 |
{
|
| 10 |
Task<IEnumerable<BookDto>> GetAllBooksAsync(int? categoryId = null);
|
| 11 |
Task<BookDto?> GetBookByIdAsync(int id);
|
|
|
|
| 12 |
Task<BookDto> CreateBookAsync(BookCreateRequest request);
|
| 13 |
Task<BookDto?> UpdateBookAsync(int id, BookUpdateRequest request);
|
| 14 |
Task<bool> DeleteBookAsync(int id);
|
|
@@ -47,6 +48,15 @@ namespace Backend.Features.Books
|
|
| 47 |
return MapToDto(book);
|
| 48 |
}
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
public async Task<BookDto> CreateBookAsync(BookCreateRequest request)
|
| 51 |
{
|
| 52 |
if (await _context.Books.AnyAsync(b => b.Isbn == request.Isbn))
|
|
|
|
| 9 |
{
|
| 10 |
Task<IEnumerable<BookDto>> GetAllBooksAsync(int? categoryId = null);
|
| 11 |
Task<BookDto?> GetBookByIdAsync(int id);
|
| 12 |
+
Task<IEnumerable<BookDto>> GetBooksByIdsAsync(List<int> ids);
|
| 13 |
Task<BookDto> CreateBookAsync(BookCreateRequest request);
|
| 14 |
Task<BookDto?> UpdateBookAsync(int id, BookUpdateRequest request);
|
| 15 |
Task<bool> DeleteBookAsync(int id);
|
|
|
|
| 48 |
return MapToDto(book);
|
| 49 |
}
|
| 50 |
|
| 51 |
+
public async Task<IEnumerable<BookDto>> GetBooksByIdsAsync(List<int> ids)
|
| 52 |
+
{
|
| 53 |
+
var books = await _context.Books
|
| 54 |
+
.Include(b => b.Categories)
|
| 55 |
+
.Where(b => ids.Contains(b.Id) && b.IsActive)
|
| 56 |
+
.ToListAsync();
|
| 57 |
+
return books.Select(b => MapToDto(b));
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
public async Task<BookDto> CreateBookAsync(BookCreateRequest request)
|
| 61 |
{
|
| 62 |
if (await _context.Books.AnyAsync(b => b.Isbn == request.Isbn))
|
Backend/Features/Borrowing/BorrowingService.cs
CHANGED
|
@@ -2,6 +2,7 @@ using DbConnect.Data;
|
|
| 2 |
using DbConnect.Entities;
|
| 3 |
using Microsoft.EntityFrameworkCore;
|
| 4 |
using Backend.Features.Loyalty;
|
|
|
|
| 5 |
|
| 6 |
namespace Backend.Features.Borrowings
|
| 7 |
{
|
|
@@ -18,12 +19,14 @@ namespace Backend.Features.Borrowings
|
|
| 18 |
{
|
| 19 |
private readonly AppDbContext _context;
|
| 20 |
private readonly ILoyaltyService _loyaltyService;
|
|
|
|
| 21 |
private const decimal FinePerDay = 500;
|
| 22 |
|
| 23 |
-
public BorrowingService(AppDbContext context, ILoyaltyService loyaltyService)
|
| 24 |
{
|
| 25 |
_context = context;
|
| 26 |
_loyaltyService = loyaltyService;
|
|
|
|
| 27 |
}
|
| 28 |
|
| 29 |
public async Task<BorrowingDto> BorrowBookAsync(Guid userId, int bookId)
|
|
@@ -142,6 +145,32 @@ namespace Backend.Features.Borrowings
|
|
| 142 |
|
| 143 |
await _context.SaveChangesAsync();
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
// Loyalty Integration: Send RETURN event
|
| 146 |
string externalUserId = borrowing.UserId.ToString();
|
| 147 |
string userMobile = borrowing.User?.PhoneNumber ?? "0000000000";
|
|
|
|
| 2 |
using DbConnect.Entities;
|
| 3 |
using Microsoft.EntityFrameworkCore;
|
| 4 |
using Backend.Features.Loyalty;
|
| 5 |
+
using Backend.Features.Notification;
|
| 6 |
|
| 7 |
namespace Backend.Features.Borrowings
|
| 8 |
{
|
|
|
|
| 19 |
{
|
| 20 |
private readonly AppDbContext _context;
|
| 21 |
private readonly ILoyaltyService _loyaltyService;
|
| 22 |
+
private readonly INotificationService _notificationService;
|
| 23 |
private const decimal FinePerDay = 500;
|
| 24 |
|
| 25 |
+
public BorrowingService(AppDbContext context, ILoyaltyService loyaltyService, INotificationService notificationService)
|
| 26 |
{
|
| 27 |
_context = context;
|
| 28 |
_loyaltyService = loyaltyService;
|
| 29 |
+
_notificationService = notificationService;
|
| 30 |
}
|
| 31 |
|
| 32 |
public async Task<BorrowingDto> BorrowBookAsync(Guid userId, int bookId)
|
|
|
|
| 145 |
|
| 146 |
await _context.SaveChangesAsync();
|
| 147 |
|
| 148 |
+
// Wishlist Notification Trigger
|
| 149 |
+
try
|
| 150 |
+
{
|
| 151 |
+
var interestedUsers = await _context.WishlistItems
|
| 152 |
+
.Include(w => w.User)
|
| 153 |
+
.Where(w => w.BookId == book.Id)
|
| 154 |
+
.ToListAsync();
|
| 155 |
+
|
| 156 |
+
foreach (var wishlistEntry in interestedUsers)
|
| 157 |
+
{
|
| 158 |
+
await _notificationService.SendAndSaveNotificationAsync(
|
| 159 |
+
wishlistEntry.UserId,
|
| 160 |
+
wishlistEntry.User.FcmToken,
|
| 161 |
+
"Book Available!",
|
| 162 |
+
$"Good news! '{book.Title}' is now available for borrowing. Grab it before someone else does!",
|
| 163 |
+
"Success",
|
| 164 |
+
"/Books",
|
| 165 |
+
"View Book"
|
| 166 |
+
);
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
catch (Exception ex)
|
| 170 |
+
{
|
| 171 |
+
Console.WriteLine($"Error sending wishlist notifications: {ex.Message}");
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
// Loyalty Integration: Send RETURN event
|
| 175 |
string externalUserId = borrowing.UserId.ToString();
|
| 176 |
string userMobile = borrowing.User?.PhoneNumber ?? "0000000000";
|
Backend/Features/Notification/INotificationService.cs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using System.Threading.Tasks;
|
| 2 |
+
using System.Collections.Generic;
|
| 3 |
+
|
| 4 |
+
namespace Backend.Features.Notification;
|
| 5 |
+
|
| 6 |
+
public interface INotificationService
|
| 7 |
+
{
|
| 8 |
+
Task<bool> SendNotificationAsync(string token, string title, string body, Dictionary<string, string>? data = null);
|
| 9 |
+
Task<bool> SendAndSaveNotificationAsync(Guid userId, string? token, string title, string body, string type = "Info", string? actionLink = null, string? actionText = null);
|
| 10 |
+
Task<List<NotificationDto>> GetUserNotificationsAsync(Guid userId);
|
| 11 |
+
Task<bool> MarkAllAsReadAsync(Guid userId);
|
| 12 |
+
Task<int> GetUnreadCountAsync(Guid userId);
|
| 13 |
+
}
|
Backend/Features/Notification/NotificationBackgroundService.cs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using Microsoft.Extensions.DependencyInjection;
|
| 2 |
+
using Microsoft.Extensions.Hosting;
|
| 3 |
+
using System;
|
| 4 |
+
using System.Linq;
|
| 5 |
+
using System.Threading;
|
| 6 |
+
using System.Threading.Tasks;
|
| 7 |
+
using DbConnect.Data;
|
| 8 |
+
using Microsoft.EntityFrameworkCore;
|
| 9 |
+
|
| 10 |
+
namespace Backend.Features.Notification;
|
| 11 |
+
|
| 12 |
+
public class NotificationBackgroundService : BackgroundService
|
| 13 |
+
{
|
| 14 |
+
private readonly IServiceProvider _serviceProvider;
|
| 15 |
+
private readonly TimeSpan _checkInterval = TimeSpan.FromHours(24); // Check once a day
|
| 16 |
+
|
| 17 |
+
public NotificationBackgroundService(IServiceProvider serviceProvider)
|
| 18 |
+
{
|
| 19 |
+
_serviceProvider = serviceProvider;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
| 23 |
+
{
|
| 24 |
+
while (!stoppingToken.IsCancellationRequested)
|
| 25 |
+
{
|
| 26 |
+
try
|
| 27 |
+
{
|
| 28 |
+
using (var scope = _serviceProvider.CreateScope())
|
| 29 |
+
{
|
| 30 |
+
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
| 31 |
+
var notificationService = scope.ServiceProvider.GetRequiredService<INotificationService>();
|
| 32 |
+
|
| 33 |
+
await CheckSubscriptionExpiries(context, notificationService);
|
| 34 |
+
await CheckBorrowingReminders(context, notificationService);
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
catch (Exception ex)
|
| 38 |
+
{
|
| 39 |
+
Console.WriteLine($"Error in NotificationBackgroundService: {ex.Message}");
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
await Task.Delay(_checkInterval, stoppingToken);
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
private async Task CheckSubscriptionExpiries(AppDbContext context, INotificationService notificationService)
|
| 47 |
+
{
|
| 48 |
+
var targetDate = DateTime.Today.AddDays(25);
|
| 49 |
+
|
| 50 |
+
var expiringSubscriptions = await context.UserSubscriptions
|
| 51 |
+
.Include(s => s.User)
|
| 52 |
+
.Where(s => s.IsActive && s.ExpiryDate.Date <= targetDate)
|
| 53 |
+
.ToListAsync();
|
| 54 |
+
|
| 55 |
+
foreach (var sub in expiringSubscriptions)
|
| 56 |
+
{
|
| 57 |
+
// Check if notification already sent TODAY to avoid duplicates on restart
|
| 58 |
+
var alreadySentToday = await context.Notifications.AnyAsync(n =>
|
| 59 |
+
n.UserId == sub.UserId &&
|
| 60 |
+
n.Title == "Subscription Renewal Reminder" &&
|
| 61 |
+
n.CreatedAt >= DateTime.Today);
|
| 62 |
+
|
| 63 |
+
if (alreadySentToday) continue;
|
| 64 |
+
|
| 65 |
+
string title = "Subscription Renewal Reminder";
|
| 66 |
+
string message = $"Hello {sub.User.FullName}, your library subscription will expire soon ({sub.ExpiryDate:MMM dd, yyyy}). Renew now to avoid interruption!";
|
| 67 |
+
|
| 68 |
+
await notificationService.SendAndSaveNotificationAsync(
|
| 69 |
+
sub.UserId,
|
| 70 |
+
sub.User.FcmToken,
|
| 71 |
+
title,
|
| 72 |
+
message,
|
| 73 |
+
"Warning",
|
| 74 |
+
"/Membership",
|
| 75 |
+
"Renew Now");
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
private async Task CheckBorrowingReminders(AppDbContext context, INotificationService notificationService)
|
| 80 |
+
{
|
| 81 |
+
var targetDate = DateTime.Today.AddDays(10);
|
| 82 |
+
|
| 83 |
+
var dueBorrowings = await context.Borrowings
|
| 84 |
+
.Include(b => b.User)
|
| 85 |
+
.Include(b => b.Book)
|
| 86 |
+
.Where(b => b.Status == "Borrowed" && b.DueDate.Date <= targetDate)
|
| 87 |
+
.ToListAsync();
|
| 88 |
+
|
| 89 |
+
foreach (var loan in dueBorrowings)
|
| 90 |
+
{
|
| 91 |
+
// Check if notification already sent for this specific book TODAY
|
| 92 |
+
var alreadySentToday = await context.Notifications.AnyAsync(n =>
|
| 93 |
+
n.UserId == loan.UserId &&
|
| 94 |
+
n.Title == "Book Return Reminder" &&
|
| 95 |
+
n.Message.Contains(loan.Book.Title) &&
|
| 96 |
+
n.CreatedAt >= DateTime.Today);
|
| 97 |
+
|
| 98 |
+
if (alreadySentToday) continue;
|
| 99 |
+
|
| 100 |
+
string title = "Book Return Reminder";
|
| 101 |
+
string message = $"Hello {loan.User.FullName}, the book '{loan.Book.Title}' is due soon ({loan.DueDate:MMM dd, yyyy}). Please remember to return it on time!";
|
| 102 |
+
|
| 103 |
+
await notificationService.SendAndSaveNotificationAsync(
|
| 104 |
+
loan.UserId,
|
| 105 |
+
loan.User.FcmToken,
|
| 106 |
+
title,
|
| 107 |
+
message,
|
| 108 |
+
"Info",
|
| 109 |
+
"/Borrowings",
|
| 110 |
+
"View My Loans");
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
}
|
Backend/Features/Notification/NotificationModels.cs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
namespace Backend.Features.Notification;
|
| 2 |
+
|
| 3 |
+
public class SubscriptionExpiryNotificationRequest
|
| 4 |
+
{
|
| 5 |
+
public Guid UserId { get; set; }
|
| 6 |
+
public string FcmToken { get; set; } = null!;
|
| 7 |
+
public string FullName { get; set; } = null!;
|
| 8 |
+
public int DaysRemaining { get; set; }
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
public class ReturnReminderNotificationRequest
|
| 12 |
+
{
|
| 13 |
+
public Guid UserId { get; set; }
|
| 14 |
+
public string FcmToken { get; set; } = null!;
|
| 15 |
+
public string FullName { get; set; } = null!;
|
| 16 |
+
public string BookTitle { get; set; } = null!;
|
| 17 |
+
public DateTime DueDate { get; set; }
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
public class NotificationDto
|
| 21 |
+
{
|
| 22 |
+
public int Id { get; set; }
|
| 23 |
+
public string Title { get; set; } = null!;
|
| 24 |
+
public string Message { get; set; } = null!;
|
| 25 |
+
public string Type { get; set; } = null!;
|
| 26 |
+
public string? ActionLink { get; set; }
|
| 27 |
+
public string? ActionText { get; set; }
|
| 28 |
+
public bool IsRead { get; set; }
|
| 29 |
+
public DateTime CreatedAt { get; set; }
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
public class NotificationResponse
|
| 33 |
+
{
|
| 34 |
+
public bool Success { get; set; }
|
| 35 |
+
public string Message { get; set; } = null!;
|
| 36 |
+
}
|
Backend/Features/Notification/NotificationService.cs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using FirebaseAdmin.Messaging;
|
| 2 |
+
using System;
|
| 3 |
+
using System.Collections.Generic;
|
| 4 |
+
using System.Linq;
|
| 5 |
+
using System.Threading.Tasks;
|
| 6 |
+
using DbConnect.Data;
|
| 7 |
+
using DbConnect.Entities;
|
| 8 |
+
using Microsoft.EntityFrameworkCore;
|
| 9 |
+
|
| 10 |
+
namespace Backend.Features.Notification;
|
| 11 |
+
|
| 12 |
+
public class NotificationService : INotificationService
|
| 13 |
+
{
|
| 14 |
+
private readonly AppDbContext _context;
|
| 15 |
+
|
| 16 |
+
public NotificationService(AppDbContext context)
|
| 17 |
+
{
|
| 18 |
+
_context = context;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
public async Task<bool> SendNotificationAsync(string token, string title, string body, Dictionary<string, string>? data = null)
|
| 22 |
+
{
|
| 23 |
+
var message = new Message()
|
| 24 |
+
{
|
| 25 |
+
Token = token,
|
| 26 |
+
Notification = new FirebaseAdmin.Messaging.Notification()
|
| 27 |
+
{
|
| 28 |
+
Title = title,
|
| 29 |
+
Body = body
|
| 30 |
+
},
|
| 31 |
+
Data = data
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
try
|
| 35 |
+
{
|
| 36 |
+
string response = await FirebaseMessaging.DefaultInstance.SendAsync(message);
|
| 37 |
+
return !string.IsNullOrEmpty(response);
|
| 38 |
+
}
|
| 39 |
+
catch (Exception ex)
|
| 40 |
+
{
|
| 41 |
+
Console.WriteLine($"Error sending FCM message: {ex.Message}");
|
| 42 |
+
return false;
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
public async Task<bool> SendAndSaveNotificationAsync(Guid userId, string? token, string title, string body, string type = "Info", string? actionLink = null, string? actionText = null)
|
| 47 |
+
{
|
| 48 |
+
// 0. Verify User Exists
|
| 49 |
+
var userExists = await _context.Users.AnyAsync(u => u.Id == userId);
|
| 50 |
+
if (!userExists)
|
| 51 |
+
{
|
| 52 |
+
Console.WriteLine($"[NotificationService] Error: User with ID {userId} not found. Cannot save notification.");
|
| 53 |
+
return false;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// 1. Save to Database
|
| 57 |
+
var notification = new DbConnect.Entities.Notification
|
| 58 |
+
{
|
| 59 |
+
UserId = userId,
|
| 60 |
+
Title = title,
|
| 61 |
+
Message = body,
|
| 62 |
+
Type = type,
|
| 63 |
+
ActionLink = actionLink,
|
| 64 |
+
ActionText = actionText,
|
| 65 |
+
CreatedAt = DateTime.Now,
|
| 66 |
+
IsRead = false
|
| 67 |
+
};
|
| 68 |
+
|
| 69 |
+
_context.Notifications.Add(notification);
|
| 70 |
+
await _context.SaveChangesAsync();
|
| 71 |
+
|
| 72 |
+
// 2. Send via FCM if token exists
|
| 73 |
+
if (!string.IsNullOrEmpty(token))
|
| 74 |
+
{
|
| 75 |
+
var data = new Dictionary<string, string>
|
| 76 |
+
{
|
| 77 |
+
{ "type", type }
|
| 78 |
+
};
|
| 79 |
+
if (!string.IsNullOrEmpty(actionLink)) data.Add("actionLink", actionLink);
|
| 80 |
+
if (!string.IsNullOrEmpty(actionText)) data.Add("actionText", actionText);
|
| 81 |
+
|
| 82 |
+
return await SendNotificationAsync(token, title, body, data);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
return true;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
public async Task<List<NotificationDto>> GetUserNotificationsAsync(Guid userId)
|
| 89 |
+
{
|
| 90 |
+
return await _context.Notifications
|
| 91 |
+
.Where(n => n.UserId == userId)
|
| 92 |
+
.OrderByDescending(n => n.CreatedAt)
|
| 93 |
+
.Select(n => new NotificationDto
|
| 94 |
+
{
|
| 95 |
+
Id = n.Id,
|
| 96 |
+
Title = n.Title,
|
| 97 |
+
Message = n.Message,
|
| 98 |
+
Type = n.Type,
|
| 99 |
+
ActionLink = n.ActionLink,
|
| 100 |
+
ActionText = n.ActionText,
|
| 101 |
+
IsRead = n.IsRead,
|
| 102 |
+
CreatedAt = n.CreatedAt
|
| 103 |
+
})
|
| 104 |
+
.ToListAsync();
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
public async Task<bool> MarkAllAsReadAsync(Guid userId)
|
| 108 |
+
{
|
| 109 |
+
var unread = await _context.Notifications
|
| 110 |
+
.Where(n => n.UserId == userId && !n.IsRead)
|
| 111 |
+
.ToListAsync();
|
| 112 |
+
|
| 113 |
+
if (unread.Any())
|
| 114 |
+
{
|
| 115 |
+
foreach (var n in unread)
|
| 116 |
+
{
|
| 117 |
+
n.IsRead = true;
|
| 118 |
+
}
|
| 119 |
+
await _context.SaveChangesAsync();
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
return true;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
public async Task<int> GetUnreadCountAsync(Guid userId)
|
| 126 |
+
{
|
| 127 |
+
return await _context.Notifications
|
| 128 |
+
.CountAsync(n => n.UserId == userId && !n.IsRead);
|
| 129 |
+
}
|
| 130 |
+
}
|
Backend/Features/Notification/NotificationsController.cs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using Microsoft.AspNetCore.Mvc;
|
| 2 |
+
using System;
|
| 3 |
+
using System.Collections.Generic;
|
| 4 |
+
using System.Linq;
|
| 5 |
+
using System.Security.Claims;
|
| 6 |
+
using System.Threading.Tasks;
|
| 7 |
+
|
| 8 |
+
namespace Backend.Features.Notification;
|
| 9 |
+
|
| 10 |
+
[ApiController]
|
| 11 |
+
[Route("api/[controller]")]
|
| 12 |
+
public class NotificationsController : ControllerBase
|
| 13 |
+
{
|
| 14 |
+
private readonly INotificationService _notificationService;
|
| 15 |
+
|
| 16 |
+
public NotificationsController(INotificationService notificationService)
|
| 17 |
+
{
|
| 18 |
+
_notificationService = notificationService;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
[HttpGet]
|
| 22 |
+
public async Task<IActionResult> GetUserNotifications()
|
| 23 |
+
{
|
| 24 |
+
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
| 25 |
+
if (string.IsNullOrEmpty(userIdStr) || !Guid.TryParse(userIdStr, out Guid userId))
|
| 26 |
+
{
|
| 27 |
+
return Unauthorized();
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
var notifications = await _notificationService.GetUserNotificationsAsync(userId);
|
| 31 |
+
return Ok(notifications);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
[HttpGet("unread-count")]
|
| 35 |
+
public async Task<IActionResult> GetUnreadCount()
|
| 36 |
+
{
|
| 37 |
+
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
| 38 |
+
if (string.IsNullOrEmpty(userIdStr) || !Guid.TryParse(userIdStr, out Guid userId))
|
| 39 |
+
{
|
| 40 |
+
return Unauthorized();
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
var count = await _notificationService.GetUnreadCountAsync(userId);
|
| 44 |
+
return Ok(count);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
[HttpPost("mark-read")]
|
| 48 |
+
public async Task<IActionResult> MarkAllAsRead()
|
| 49 |
+
{
|
| 50 |
+
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
| 51 |
+
if (string.IsNullOrEmpty(userIdStr) || !Guid.TryParse(userIdStr, out Guid userId))
|
| 52 |
+
{
|
| 53 |
+
return Unauthorized();
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
await _notificationService.MarkAllAsReadAsync(userId);
|
| 57 |
+
return Ok();
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
[HttpPost("subscription-expiry")]
|
| 61 |
+
public async Task<IActionResult> SendSubscriptionExpiryNotification([FromBody] SubscriptionExpiryNotificationRequest request)
|
| 62 |
+
{
|
| 63 |
+
string title = "Subscription Expiry Reminder";
|
| 64 |
+
string body = $"Hello {request.FullName}, your library subscription is expiring in {request.DaysRemaining} days. Please renew it to continue enjoying our services!";
|
| 65 |
+
|
| 66 |
+
bool success = await _notificationService.SendAndSaveNotificationAsync(
|
| 67 |
+
request.UserId,
|
| 68 |
+
request.FcmToken,
|
| 69 |
+
title,
|
| 70 |
+
body,
|
| 71 |
+
"Warning",
|
| 72 |
+
"/Membership",
|
| 73 |
+
"Renew Now");
|
| 74 |
+
|
| 75 |
+
if (success)
|
| 76 |
+
return Ok(new NotificationResponse { Success = true, Message = "Notification sent and saved successfully" });
|
| 77 |
+
|
| 78 |
+
return BadRequest(new NotificationResponse { Success = false, Message = "Failed to process notification" });
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
[HttpPost("return-reminder")]
|
| 82 |
+
public async Task<IActionResult> SendReturnReminder([FromBody] ReturnReminderNotificationRequest request)
|
| 83 |
+
{
|
| 84 |
+
string title = "Book Return Reminder";
|
| 85 |
+
string body = $"Hello {request.FullName}, the book '{request.BookTitle}' is due for return on {request.DueDate:MMM dd, yyyy}. Please make sure to return it on time!";
|
| 86 |
+
|
| 87 |
+
bool success = await _notificationService.SendAndSaveNotificationAsync(
|
| 88 |
+
request.UserId,
|
| 89 |
+
request.FcmToken,
|
| 90 |
+
title,
|
| 91 |
+
body,
|
| 92 |
+
"Warning",
|
| 93 |
+
"/Borrowings",
|
| 94 |
+
"View Loans");
|
| 95 |
+
|
| 96 |
+
if (success)
|
| 97 |
+
return Ok(new NotificationResponse { Success = true, Message = "Notification sent and saved successfully" });
|
| 98 |
+
|
| 99 |
+
return BadRequest(new NotificationResponse { Success = false, Message = "Failed to process notification" });
|
| 100 |
+
}
|
| 101 |
+
}
|
Backend/Features/Users/UserController.cs
CHANGED
|
@@ -72,6 +72,26 @@ namespace Backend.Features.Users
|
|
| 72 |
if (!success) return NotFound();
|
| 73 |
return NoContent();
|
| 74 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
}
|
| 76 |
}
|
| 77 |
|
|
|
|
| 72 |
if (!success) return NotFound();
|
| 73 |
return NoContent();
|
| 74 |
}
|
| 75 |
+
|
| 76 |
+
[HttpPost("fcm-token")]
|
| 77 |
+
[Authorize]
|
| 78 |
+
public async Task<IActionResult> UpdateFcmToken([FromBody] FcmTokenUpdateRequest request)
|
| 79 |
+
{
|
| 80 |
+
var userIdStr = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
| 81 |
+
if (string.IsNullOrEmpty(userIdStr) || !Guid.TryParse(userIdStr, out Guid userId))
|
| 82 |
+
{
|
| 83 |
+
return Unauthorized();
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
var success = await _userService.UpdateFcmTokenAsync(userId, request.FcmToken);
|
| 87 |
+
if (!success) return NotFound();
|
| 88 |
+
return Ok(new { message = "FCM token updated successfully" });
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
public class FcmTokenUpdateRequest
|
| 93 |
+
{
|
| 94 |
+
public string FcmToken { get; set; } = null!;
|
| 95 |
}
|
| 96 |
}
|
| 97 |
|
Backend/Features/Users/UserService.cs
CHANGED
|
@@ -18,6 +18,7 @@ namespace Backend.Features.Users
|
|
| 18 |
Task<UserDto?> UpdateUserAsync(Guid id, UserUpdateRequest request);
|
| 19 |
Task<bool> UpdateUserRoleAsync(Guid id, string role);
|
| 20 |
Task<bool> DeleteUserAsync(Guid id);
|
|
|
|
| 21 |
}
|
| 22 |
|
| 23 |
public class UserService : IUserService
|
|
@@ -138,6 +139,18 @@ namespace Backend.Features.Users
|
|
| 138 |
return true;
|
| 139 |
}
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
private static UserDto MapToDto(User user)
|
| 142 |
{
|
| 143 |
return new UserDto
|
|
|
|
| 18 |
Task<UserDto?> UpdateUserAsync(Guid id, UserUpdateRequest request);
|
| 19 |
Task<bool> UpdateUserRoleAsync(Guid id, string role);
|
| 20 |
Task<bool> DeleteUserAsync(Guid id);
|
| 21 |
+
Task<bool> UpdateFcmTokenAsync(Guid id, string fcmToken);
|
| 22 |
}
|
| 23 |
|
| 24 |
public class UserService : IUserService
|
|
|
|
| 139 |
return true;
|
| 140 |
}
|
| 141 |
|
| 142 |
+
public async Task<bool> UpdateFcmTokenAsync(Guid id, string fcmToken)
|
| 143 |
+
{
|
| 144 |
+
var user = await _context.Users.FindAsync(id);
|
| 145 |
+
if (user == null) return false;
|
| 146 |
+
|
| 147 |
+
user.FcmToken = fcmToken;
|
| 148 |
+
user.UpdatedAt = DateTime.UtcNow;
|
| 149 |
+
|
| 150 |
+
await _context.SaveChangesAsync();
|
| 151 |
+
return true;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
private static UserDto MapToDto(User user)
|
| 155 |
{
|
| 156 |
return new UserDto
|
Backend/Features/Wishlist/WishlistController.cs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using Microsoft.AspNetCore.Authorization;
|
| 2 |
+
using Microsoft.AspNetCore.Mvc;
|
| 3 |
+
using System.Security.Claims;
|
| 4 |
+
|
| 5 |
+
namespace Backend.Features.Wishlist
|
| 6 |
+
{
|
| 7 |
+
[ApiController]
|
| 8 |
+
[Route("api/wishlist")]
|
| 9 |
+
[Authorize]
|
| 10 |
+
public class WishlistController : ControllerBase
|
| 11 |
+
{
|
| 12 |
+
private readonly IWishlistService _wishlistService;
|
| 13 |
+
|
| 14 |
+
public WishlistController(IWishlistService wishlistService)
|
| 15 |
+
{
|
| 16 |
+
_wishlistService = wishlistService;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
[HttpPost("sync")]
|
| 20 |
+
public async Task<IActionResult> SyncWishlist([FromBody] List<int> bookIds)
|
| 21 |
+
{
|
| 22 |
+
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
| 23 |
+
if (string.IsNullOrEmpty(userIdStr) || !Guid.TryParse(userIdStr, out Guid userId))
|
| 24 |
+
{
|
| 25 |
+
return Unauthorized();
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
var success = await _wishlistService.SyncWishlistAsync(userId, bookIds);
|
| 29 |
+
if (!success) return BadRequest("Failed to sync wishlist");
|
| 30 |
+
|
| 31 |
+
return Ok(new { message = "Wishlist synced successfully" });
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
[HttpGet]
|
| 35 |
+
public async Task<IActionResult> GetWishlist()
|
| 36 |
+
{
|
| 37 |
+
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
| 38 |
+
if (string.IsNullOrEmpty(userIdStr) || !Guid.TryParse(userIdStr, out Guid userId))
|
| 39 |
+
{
|
| 40 |
+
return Unauthorized();
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
var bookIds = await _wishlistService.GetWishlistBookIdsAsync(userId);
|
| 44 |
+
return Ok(bookIds);
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
}
|
Backend/Features/Wishlist/WishlistService.cs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using DbConnect.Data;
|
| 2 |
+
using DbConnect.Entities;
|
| 3 |
+
using Microsoft.EntityFrameworkCore;
|
| 4 |
+
using System;
|
| 5 |
+
using System.Collections.Generic;
|
| 6 |
+
using System.Linq;
|
| 7 |
+
using System.Threading.Tasks;
|
| 8 |
+
|
| 9 |
+
namespace Backend.Features.Wishlist
|
| 10 |
+
{
|
| 11 |
+
public interface IWishlistService
|
| 12 |
+
{
|
| 13 |
+
Task<bool> SyncWishlistAsync(Guid userId, List<int> bookIds);
|
| 14 |
+
Task<List<int>> GetWishlistBookIdsAsync(Guid userId);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
public class WishlistService : IWishlistService
|
| 18 |
+
{
|
| 19 |
+
private readonly AppDbContext _context;
|
| 20 |
+
|
| 21 |
+
public WishlistService(AppDbContext context)
|
| 22 |
+
{
|
| 23 |
+
_context = context;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
public async Task<bool> SyncWishlistAsync(Guid userId, List<int> bookIds)
|
| 27 |
+
{
|
| 28 |
+
// 1. Remove existing wishlist items for this user
|
| 29 |
+
var existing = await _context.WishlistItems
|
| 30 |
+
.Where(w => w.UserId == userId)
|
| 31 |
+
.ToListAsync();
|
| 32 |
+
|
| 33 |
+
_context.WishlistItems.RemoveRange(existing);
|
| 34 |
+
|
| 35 |
+
// 2. Add new wishlist items
|
| 36 |
+
var newItems = bookIds.Select(bookId => new WishlistItem
|
| 37 |
+
{
|
| 38 |
+
UserId = userId,
|
| 39 |
+
BookId = bookId,
|
| 40 |
+
AddedAt = DateTime.UtcNow
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
await _context.WishlistItems.AddRangeAsync(newItems);
|
| 44 |
+
await _context.SaveChangesAsync();
|
| 45 |
+
|
| 46 |
+
return true;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
public async Task<List<int>> GetWishlistBookIdsAsync(Guid userId)
|
| 50 |
+
{
|
| 51 |
+
return await _context.WishlistItems
|
| 52 |
+
.Where(w => w.UserId == userId)
|
| 53 |
+
.Select(w => w.BookId)
|
| 54 |
+
.ToListAsync();
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
}
|
Backend/Program.cs
CHANGED
|
@@ -12,9 +12,19 @@ using Backend.Features.Subscriptions;
|
|
| 12 |
using Backend.Features.Categories;
|
| 13 |
using Backend.Features.Borrowings;
|
| 14 |
using Backend.Features.Loyalty;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
var builder = WebApplication.CreateBuilder(args);
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
builder.Services.AddControllers();
|
| 20 |
builder.Services.AddAuthorization();
|
|
@@ -57,6 +67,9 @@ builder.Services.AddScoped<IBookService, BookService>();
|
|
| 57 |
builder.Services.AddScoped<ISubscriptionService, SubscriptionService>();
|
| 58 |
builder.Services.AddScoped<ICategoryService, CategoryService>();
|
| 59 |
builder.Services.AddScoped<IBorrowingService, BorrowingService>();
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
// Register Loyalty API Client
|
| 62 |
builder.Services.AddHttpClient<ILoyaltyService, LoyaltyService>(client =>
|
|
|
|
| 12 |
using Backend.Features.Categories;
|
| 13 |
using Backend.Features.Borrowings;
|
| 14 |
using Backend.Features.Loyalty;
|
| 15 |
+
using Backend.Features.Notification;
|
| 16 |
+
using Backend.Features.Wishlist;
|
| 17 |
+
using FirebaseAdmin;
|
| 18 |
+
using Google.Apis.Auth.OAuth2;
|
| 19 |
|
| 20 |
var builder = WebApplication.CreateBuilder(args);
|
| 21 |
|
| 22 |
+
// Firebase Admin SDK Initialization
|
| 23 |
+
FirebaseApp.Create(new AppOptions()
|
| 24 |
+
{
|
| 25 |
+
Credential = GoogleCredential.FromFile("LibraryFirebase.json")
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
|
| 29 |
builder.Services.AddControllers();
|
| 30 |
builder.Services.AddAuthorization();
|
|
|
|
| 67 |
builder.Services.AddScoped<ISubscriptionService, SubscriptionService>();
|
| 68 |
builder.Services.AddScoped<ICategoryService, CategoryService>();
|
| 69 |
builder.Services.AddScoped<IBorrowingService, BorrowingService>();
|
| 70 |
+
builder.Services.AddScoped<INotificationService, NotificationService>();
|
| 71 |
+
builder.Services.AddScoped<IWishlistService, WishlistService>();
|
| 72 |
+
builder.Services.AddHostedService<NotificationBackgroundService>();
|
| 73 |
|
| 74 |
// Register Loyalty API Client
|
| 75 |
builder.Services.AddHttpClient<ILoyaltyService, LoyaltyService>(client =>
|
BlazorFrontend/BlazorFrontend.csproj
CHANGED
|
@@ -7,6 +7,7 @@
|
|
| 7 |
</PropertyGroup>
|
| 8 |
|
| 9 |
<ItemGroup>
|
|
|
|
| 10 |
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.17.0" />
|
| 11 |
</ItemGroup>
|
| 12 |
|
|
|
|
| 7 |
</PropertyGroup>
|
| 8 |
|
| 9 |
<ItemGroup>
|
| 10 |
+
<PackageReference Include="FirebaseAdmin" Version="3.5.0" />
|
| 11 |
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.17.0" />
|
| 12 |
</ItemGroup>
|
| 13 |
|
BlazorFrontend/Components/App.razor
CHANGED
|
@@ -109,6 +109,7 @@
|
|
| 109 |
}, 3000);
|
| 110 |
}
|
| 111 |
</script>
|
|
|
|
| 112 |
</body>
|
| 113 |
|
| 114 |
</html>
|
|
|
|
| 109 |
}, 3000);
|
| 110 |
}
|
| 111 |
</script>
|
| 112 |
+
<script type="module" src="firebase-init.js"></script>
|
| 113 |
</body>
|
| 114 |
|
| 115 |
</html>
|
BlazorFrontend/Components/Layout/MainLayout.razor
CHANGED
|
@@ -127,12 +127,18 @@
|
|
| 127 |
</div>
|
| 128 |
</div>
|
| 129 |
<div class="ml-4 flex items-center md:ml-6 gap-4">
|
| 130 |
-
<
|
| 131 |
<span class="sr-only">View notifications</span>
|
| 132 |
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 133 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
| 134 |
</svg>
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
<AuthorizeView>
|
| 137 |
<Authorized>
|
| 138 |
<a href="/logout" class="btn-secondary py-1.5">Logout</a>
|
|
@@ -169,8 +175,12 @@
|
|
| 169 |
<a class="dismiss cursor-pointer">🗙</a>
|
| 170 |
</div>
|
| 171 |
|
|
|
|
|
|
|
|
|
|
| 172 |
@code {
|
| 173 |
private bool _isReady;
|
|
|
|
| 174 |
|
| 175 |
protected override async Task OnAfterRenderAsync(bool firstRender)
|
| 176 |
{
|
|
@@ -180,6 +190,27 @@
|
|
| 180 |
if (jwtProvider != null)
|
| 181 |
{
|
| 182 |
await jwtProvider.LoadTokenAsync();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
}
|
| 184 |
|
| 185 |
_isReady = true;
|
|
|
|
| 127 |
</div>
|
| 128 |
</div>
|
| 129 |
<div class="ml-4 flex items-center md:ml-6 gap-4">
|
| 130 |
+
<a href="notifications" class="relative bg-white p-1 rounded-full text-mystic-400 hover:text-mystic-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-mystic-900">
|
| 131 |
<span class="sr-only">View notifications</span>
|
| 132 |
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 133 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
| 134 |
</svg>
|
| 135 |
+
@if (_unreadCount > 0)
|
| 136 |
+
{
|
| 137 |
+
<span class="absolute top-0 right-0 block h-4 w-4 rounded-full bg-rose-500 text-[10px] font-bold text-white flex items-center justify-center border-2 border-white transform translate-x-1/2 -translate-y-1/2">
|
| 138 |
+
@(_unreadCount > 9 ? "9+" : _unreadCount)
|
| 139 |
+
</span>
|
| 140 |
+
}
|
| 141 |
+
</a>
|
| 142 |
<AuthorizeView>
|
| 143 |
<Authorized>
|
| 144 |
<a href="/logout" class="btn-secondary py-1.5">Logout</a>
|
|
|
|
| 175 |
<a class="dismiss cursor-pointer">🗙</a>
|
| 176 |
</div>
|
| 177 |
|
| 178 |
+
@inject BlazorFrontend.Services.LibraryApiClient ApiClient
|
| 179 |
+
@inject IJSRuntime JS
|
| 180 |
+
|
| 181 |
@code {
|
| 182 |
private bool _isReady;
|
| 183 |
+
private int _unreadCount;
|
| 184 |
|
| 185 |
protected override async Task OnAfterRenderAsync(bool firstRender)
|
| 186 |
{
|
|
|
|
| 190 |
if (jwtProvider != null)
|
| 191 |
{
|
| 192 |
await jwtProvider.LoadTokenAsync();
|
| 193 |
+
|
| 194 |
+
// Fetch unread count if authenticated
|
| 195 |
+
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
| 196 |
+
if (authState.User.Identity?.IsAuthenticated == true)
|
| 197 |
+
{
|
| 198 |
+
_unreadCount = await ApiClient.GetUnreadNotificationCountAsync();
|
| 199 |
+
|
| 200 |
+
// Update FCM Token
|
| 201 |
+
try
|
| 202 |
+
{
|
| 203 |
+
var token = await JS.InvokeAsync<string>("window.getFcmToken");
|
| 204 |
+
if (!string.IsNullOrEmpty(token))
|
| 205 |
+
{
|
| 206 |
+
await ApiClient.UpdateFcmTokenAsync(token);
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
catch (Exception ex)
|
| 210 |
+
{
|
| 211 |
+
Console.WriteLine($"Error updating FCM token: {ex.Message}");
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
}
|
| 215 |
|
| 216 |
_isReady = true;
|
BlazorFrontend/Components/Pages/FcmTest.razor
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@page "/fcm-test"
|
| 2 |
+
@inject IJSRuntime JSRuntime
|
| 3 |
+
@attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
| 4 |
+
|
| 5 |
+
<div class="p-6 max-w-4xl mx-auto">
|
| 6 |
+
<div class="card p-6 bg-white shadow-md rounded-lg">
|
| 7 |
+
<h1 class="text-2xl font-bold text-mystic-900 mb-4">FCM Token Test</h1>
|
| 8 |
+
|
| 9 |
+
<p class="text-mystic-600 mb-6">
|
| 10 |
+
Click the button below to request notification permission and generate your FCM token.
|
| 11 |
+
</p>
|
| 12 |
+
|
| 13 |
+
<div class="flex flex-col gap-4">
|
| 14 |
+
<button @onclick="GetToken" class="btn-primary w-fit">
|
| 15 |
+
Get FCM Token
|
| 16 |
+
</button>
|
| 17 |
+
|
| 18 |
+
@if (!string.IsNullOrEmpty(Token))
|
| 19 |
+
{
|
| 20 |
+
<div class="mt-4 p-4 bg-mystic-100 rounded-lg border border-mystic-200">
|
| 21 |
+
<p class="text-sm font-semibold text-mystic-900 mb-2">Your Token:</p>
|
| 22 |
+
<div class="flex gap-2">
|
| 23 |
+
<textarea readonly class="w-full p-2 text-xs font-mono bg-white border border-mystic-300 rounded" rows="4">@Token</textarea>
|
| 24 |
+
<button @onclick="CopyToken" class="btn-secondary h-fit">Copy</button>
|
| 25 |
+
</div>
|
| 26 |
+
<p class="mt-2 text-xs text-mystic-500">Copy this token and use it to test your Backend API.</p>
|
| 27 |
+
</div>
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
@if (IsLoading)
|
| 31 |
+
{
|
| 32 |
+
<p class="text-mystic-500 italic">Requesting token...</p>
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
@if (!string.IsNullOrEmpty(ErrorMessage))
|
| 36 |
+
{
|
| 37 |
+
<p class="text-red-500 text-sm">@ErrorMessage</p>
|
| 38 |
+
}
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
|
| 43 |
+
@code {
|
| 44 |
+
private string? Token { get; set; }
|
| 45 |
+
private string? ErrorMessage { get; set; }
|
| 46 |
+
private bool IsLoading { get; set; }
|
| 47 |
+
|
| 48 |
+
private async Task GetToken()
|
| 49 |
+
{
|
| 50 |
+
IsLoading = true;
|
| 51 |
+
ErrorMessage = null;
|
| 52 |
+
try
|
| 53 |
+
{
|
| 54 |
+
Token = await JSRuntime.InvokeAsync<string?>("getFcmToken");
|
| 55 |
+
if (string.IsNullOrEmpty(Token))
|
| 56 |
+
{
|
| 57 |
+
ErrorMessage = "Failed to get token. Check if you granted permission or if VAPID key is configured.";
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
catch (Exception ex)
|
| 61 |
+
{
|
| 62 |
+
ErrorMessage = $"Error: {ex.Message}";
|
| 63 |
+
}
|
| 64 |
+
finally
|
| 65 |
+
{
|
| 66 |
+
IsLoading = false;
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
private async Task CopyToken()
|
| 71 |
+
{
|
| 72 |
+
if (!string.IsNullOrEmpty(Token))
|
| 73 |
+
{
|
| 74 |
+
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", Token);
|
| 75 |
+
await JSRuntime.InvokeVoidAsync("showToast", "Token copied to clipboard!", "success");
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
}
|
BlazorFrontend/Components/Pages/Notifications.razor
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@page "/notifications"
|
| 2 |
+
@using BlazorFrontend.Models.Dtos
|
| 3 |
+
@using Microsoft.AspNetCore.Authorization
|
| 4 |
+
@using Microsoft.AspNetCore.Components.Authorization
|
| 5 |
+
@using BlazorFrontend.Services
|
| 6 |
+
@inject LibraryApiClient ApiClient
|
| 7 |
+
@inject IJSRuntime JSRuntime
|
| 8 |
+
|
| 9 |
+
<PageTitle>Notifications</PageTitle>
|
| 10 |
+
|
| 11 |
+
<AuthorizeView>
|
| 12 |
+
<Authorized>
|
| 13 |
+
|
| 14 |
+
<div class="max-w-4xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
| 15 |
+
<div class="mb-8 animate-in fade-in slide-in-from-top-4 duration-500 flex items-center justify-between">
|
| 16 |
+
<div>
|
| 17 |
+
<h1 class="text-3xl font-extrabold tracking-tight text-mystic-900">Notifications</h1>
|
| 18 |
+
<p class="text-mystic-500 font-medium mt-1">Stay updated with your library account activity.</p>
|
| 19 |
+
</div>
|
| 20 |
+
@if (_notifications != null && _notifications.Any(n => !n.IsRead))
|
| 21 |
+
{
|
| 22 |
+
<button @onclick="MarkAllAsRead" class="text-sm font-medium text-mystic-600 hover:text-mystic-900 bg-mystic-100 hover:bg-mystic-200 px-4 py-2 rounded-full transition-colors">
|
| 23 |
+
Mark all as read
|
| 24 |
+
</button>
|
| 25 |
+
}
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<div class="space-y-4 animate-in fade-in slide-in-from-bottom-8 duration-700 delay-100">
|
| 29 |
+
@if (_isLoading)
|
| 30 |
+
{
|
| 31 |
+
<div class="flex justify-center py-12">
|
| 32 |
+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-mystic-900"></div>
|
| 33 |
+
</div>
|
| 34 |
+
}
|
| 35 |
+
else if (_notifications == null || !_notifications.Any())
|
| 36 |
+
{
|
| 37 |
+
<div class="bg-white rounded-2xl p-12 text-center border border-mystic-200 shadow-sm">
|
| 38 |
+
<div class="w-16 h-16 bg-mystic-50 rounded-full flex items-center justify-center mx-auto mb-4">
|
| 39 |
+
<svg class="w-8 h-8 text-mystic-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 40 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
| 41 |
+
</svg>
|
| 42 |
+
</div>
|
| 43 |
+
<h3 class="text-lg font-bold text-mystic-900 mb-1">No notifications yet</h3>
|
| 44 |
+
<p class="text-mystic-500 text-sm">When you get updates about your books or subscription, they'll show up here.</p>
|
| 45 |
+
</div>
|
| 46 |
+
}
|
| 47 |
+
else
|
| 48 |
+
{
|
| 49 |
+
@foreach (var notif in _notifications)
|
| 50 |
+
{
|
| 51 |
+
<div class="bg-white rounded-2xl p-5 border border-mystic-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden group @(!notif.IsRead ? "bg-mystic-50/50" : "")">
|
| 52 |
+
@if (!notif.IsRead)
|
| 53 |
+
{
|
| 54 |
+
<div class="absolute left-0 top-0 bottom-0 w-1 bg-mystic-900"></div>
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
<div class="flex gap-4">
|
| 58 |
+
<div class="flex-shrink-0 mt-1">
|
| 59 |
+
@if (notif.Type == "Warning")
|
| 60 |
+
{
|
| 61 |
+
<div class="w-10 h-10 rounded-full bg-amber-100 flex items-center justify-center">
|
| 62 |
+
<svg class="w-5 h-5 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
| 63 |
+
</div>
|
| 64 |
+
}
|
| 65 |
+
else if (notif.Type == "Info")
|
| 66 |
+
{
|
| 67 |
+
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
|
| 68 |
+
<svg class="w-5 h-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
| 69 |
+
</div>
|
| 70 |
+
}
|
| 71 |
+
else
|
| 72 |
+
{
|
| 73 |
+
<div class="w-10 h-10 rounded-full bg-mystic-100 flex items-center justify-center">
|
| 74 |
+
<svg class="w-5 h-5 text-mystic-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /></svg>
|
| 75 |
+
</div>
|
| 76 |
+
}
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<div class="flex-1">
|
| 80 |
+
<div class="flex items-start justify-between gap-4">
|
| 81 |
+
<div>
|
| 82 |
+
<h4 class="text-sm font-bold text-mystic-900 @(!notif.IsRead ? "font-extrabold" : "")">@notif.Title</h4>
|
| 83 |
+
<p class="text-sm text-mystic-600 mt-1 leading-relaxed">@notif.Message</p>
|
| 84 |
+
</div>
|
| 85 |
+
<span class="text-xs font-medium text-mystic-400 whitespace-nowrap">@notif.CreatedAt.ToString("MMM dd, HH:mm")</span>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
@if (!string.IsNullOrEmpty(notif.ActionLink))
|
| 89 |
+
{
|
| 90 |
+
<div class="mt-3">
|
| 91 |
+
<a href="@notif.ActionLink" class="text-sm font-semibold text-mystic-900 hover:text-mystic-600 underline underline-offset-2">
|
| 92 |
+
@notif.ActionText
|
| 93 |
+
</a>
|
| 94 |
+
</div>
|
| 95 |
+
}
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
</Authorized>
|
| 104 |
+
<NotAuthorized>
|
| 105 |
+
<div class="max-w-4xl mx-auto py-12 px-4 text-center">
|
| 106 |
+
<h2 class="text-2xl font-bold text-mystic-900 mb-4">Please sign in to view notifications</h2>
|
| 107 |
+
<a href="/login" class="btn-primary">Sign In</a>
|
| 108 |
+
</div>
|
| 109 |
+
</NotAuthorized>
|
| 110 |
+
</AuthorizeView>
|
| 111 |
+
|
| 112 |
+
@code {
|
| 113 |
+
private List<NotificationDto> _notifications = new();
|
| 114 |
+
private bool _isLoading = true;
|
| 115 |
+
|
| 116 |
+
protected override async Task OnInitializedAsync()
|
| 117 |
+
{
|
| 118 |
+
await LoadNotifications();
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
private async Task LoadNotifications()
|
| 122 |
+
{
|
| 123 |
+
try
|
| 124 |
+
{
|
| 125 |
+
_isLoading = true;
|
| 126 |
+
StateHasChanged();
|
| 127 |
+
|
| 128 |
+
var results = await ApiClient.GetNotificationsAsync();
|
| 129 |
+
_notifications = results.ToList();
|
| 130 |
+
}
|
| 131 |
+
catch (Exception ex)
|
| 132 |
+
{
|
| 133 |
+
Console.WriteLine($"Error loading notifications: {ex.Message}");
|
| 134 |
+
_notifications = new List<NotificationDto>();
|
| 135 |
+
}
|
| 136 |
+
finally
|
| 137 |
+
{
|
| 138 |
+
_isLoading = false;
|
| 139 |
+
StateHasChanged();
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
private async Task MarkAllAsRead()
|
| 144 |
+
{
|
| 145 |
+
try
|
| 146 |
+
{
|
| 147 |
+
var success = await ApiClient.MarkNotificationsReadAsync();
|
| 148 |
+
if (success)
|
| 149 |
+
{
|
| 150 |
+
await LoadNotifications();
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
catch (Exception ex)
|
| 154 |
+
{
|
| 155 |
+
Console.WriteLine($"Error marking notifications as read: {ex.Message}");
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
}
|
BlazorFrontend/Components/Pages/Wishlist.razor
CHANGED
|
@@ -2,157 +2,165 @@
|
|
| 2 |
@using BlazorFrontend.Services
|
| 3 |
@using BlazorFrontend.Models.Dtos
|
| 4 |
@using Microsoft.AspNetCore.Authorization
|
| 5 |
-
@attribute [Authorize(Roles = "Member")]
|
| 6 |
@inject WishlistService WishlistService
|
| 7 |
@inject IJSRuntime JS
|
| 8 |
@inject NavigationManager Navigation
|
| 9 |
|
| 10 |
<PageTitle>My Wishlist & Favourites</PageTitle>
|
| 11 |
|
| 12 |
-
<
|
| 13 |
-
<
|
| 14 |
-
<
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
</
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
<div class="mb-6 border-b border-mystic-200">
|
| 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 |
-
</div>
|
| 49 |
|
| 50 |
-
@if (_isLoading)
|
| 51 |
-
{
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
}
|
| 56 |
-
else
|
| 57 |
-
{
|
| 58 |
-
<div class="animate-in fade-in duration-300">
|
| 59 |
-
@if (_activeTab == "wishlist")
|
| 60 |
{
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
<a href="Books" class="inline-block mt-4 text-sm font-bold text-mystic-900 hover:underline underline-offset-4">Browse Catalog →</a>
|
| 66 |
-
</div>
|
| 67 |
-
}
|
| 68 |
-
else
|
| 69 |
-
{
|
| 70 |
-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 71 |
-
@foreach (var book in _wishlist)
|
| 72 |
{
|
| 73 |
-
<div class="
|
| 74 |
-
<
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
}
|
| 83 |
-
</div>
|
| 84 |
-
<div class="flex-grow min-w-0">
|
| 85 |
-
<h3 class="text-sm font-bold text-mystic-900 truncate">@book.Title</h3>
|
| 86 |
-
<p class="text-xs text-mystic-500 mt-0.5 truncate">@book.Author</p>
|
| 87 |
-
<div class="mt-2">
|
| 88 |
-
<span class="inline-flex items-center rounded-full @(book.AvailableCopies > 0 ? "bg-emerald-50 text-emerald-700" : "bg-rose-50 text-rose-700") px-2 py-0.5 text-[10px] font-bold">
|
| 89 |
-
@(book.AvailableCopies > 0 ? "Now Available" : "Out of Stock")
|
| 90 |
-
</span>
|
| 91 |
-
</div>
|
| 92 |
-
<div class="mt-4 flex items-center gap-2">
|
| 93 |
-
<button @onclick='() => Navigation.NavigateTo($"/Books")' class="text-[10px] font-bold text-mystic-600 hover:text-mystic-900 uppercase tracking-tight">View Info</button>
|
| 94 |
-
<span class="text-mystic-200">|</span>
|
| 95 |
-
<button @onclick="() => RemoveFromWishlist(book.Id)" class="text-[10px] font-bold text-rose-600 hover:text-rose-700 uppercase tracking-tight">Remove</button>
|
| 96 |
-
</div>
|
| 97 |
-
</div>
|
| 98 |
-
@if (book.AvailableCopies > 0)
|
| 99 |
{
|
| 100 |
-
<div class="
|
| 101 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
</div>
|
| 103 |
}
|
| 104 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
}
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
</div>
|
| 111 |
-
<p class="text-xs font-medium">We'll notify you via Firebase when out-of-stock items become available!</p>
|
| 112 |
-
</div>
|
| 113 |
-
}
|
| 114 |
-
}
|
| 115 |
-
else
|
| 116 |
-
{
|
| 117 |
-
@if (!_favourites.Any())
|
| 118 |
-
{
|
| 119 |
-
<div class="py-20 text-center bg-mystic-50 rounded-3xl border-2 border-dashed border-mystic-200">
|
| 120 |
-
<p class="text-mystic-400 font-medium">You haven't added any favourites yet.</p>
|
| 121 |
-
<a href="Books" class="inline-block mt-4 text-sm font-bold text-mystic-900 hover:underline underline-offset-4">Browse Catalog →</a>
|
| 122 |
-
</div>
|
| 123 |
-
}
|
| 124 |
-
else
|
| 125 |
-
{
|
| 126 |
-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 127 |
-
@foreach (var book in _favourites)
|
| 128 |
{
|
| 129 |
-
<div class="
|
| 130 |
-
<
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
</div>
|
| 148 |
-
|
| 149 |
</div>
|
| 150 |
}
|
| 151 |
-
|
| 152 |
-
|
| 153 |
}
|
| 154 |
-
</
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
@code {
|
| 158 |
private string _activeTab = "wishlist";
|
|
@@ -174,6 +182,9 @@ else
|
|
| 174 |
{
|
| 175 |
_wishlist = await WishlistService.GetWishlistAsync();
|
| 176 |
_favourites = await WishlistService.GetFavouritesAsync();
|
|
|
|
|
|
|
|
|
|
| 177 |
}
|
| 178 |
|
| 179 |
private async Task RemoveFromWishlist(int id)
|
|
|
|
| 2 |
@using BlazorFrontend.Services
|
| 3 |
@using BlazorFrontend.Models.Dtos
|
| 4 |
@using Microsoft.AspNetCore.Authorization
|
|
|
|
| 5 |
@inject WishlistService WishlistService
|
| 6 |
@inject IJSRuntime JS
|
| 7 |
@inject NavigationManager Navigation
|
| 8 |
|
| 9 |
<PageTitle>My Wishlist & Favourites</PageTitle>
|
| 10 |
|
| 11 |
+
<AuthorizeView Roles="Member">
|
| 12 |
+
<Authorized>
|
| 13 |
+
<div class="mb-8 flex flex-col md:flex-row md:items-center justify-between gap-6 animate-in fade-in slide-in-from-top-4 duration-500">
|
| 14 |
+
<div>
|
| 15 |
+
<h1 class="text-3xl font-extrabold tracking-tight text-mystic-900">My Collections</h1>
|
| 16 |
+
<p class="text-mystic-600 mt-1">Track books you want to borrow and your all-time favourites.</p>
|
| 17 |
+
</div>
|
| 18 |
+
</div>
|
| 19 |
|
| 20 |
+
<div class="mb-6 border-b border-mystic-200">
|
| 21 |
+
<nav class="-mb-px flex gap-6">
|
| 22 |
+
<button @onclick='() => _activeTab = "wishlist"'
|
| 23 |
+
class='pb-3 text-sm font-semibold border-b-2 transition-all @(_activeTab == "wishlist" ? "border-amber-500 text-amber-600" : "border-transparent text-mystic-400 hover:text-amber-500")'>
|
| 24 |
+
<span class="inline-flex items-center gap-1.5">
|
| 25 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 26 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
|
| 27 |
+
</svg>
|
| 28 |
+
Wishlist
|
| 29 |
+
@if (_wishlist.Any())
|
| 30 |
+
{
|
| 31 |
+
<span class="ml-1 px-1.5 py-0.5 rounded-full bg-amber-500 text-white text-[10px]">@_wishlist.Count</span>
|
| 32 |
+
}
|
| 33 |
+
</span>
|
| 34 |
+
</button>
|
| 35 |
+
<button @onclick='() => _activeTab = "favourites"'
|
| 36 |
+
class='pb-3 text-sm font-semibold border-b-2 transition-all @(_activeTab == "favourites" ? "border-rose-500 text-rose-600" : "border-transparent text-mystic-400 hover:text-rose-500")'>
|
| 37 |
+
<span class="inline-flex items-center gap-1.5">
|
| 38 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 39 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
| 40 |
+
</svg>
|
| 41 |
+
Favourites
|
| 42 |
+
@if (_favourites.Any())
|
| 43 |
+
{
|
| 44 |
+
<span class="ml-1 px-1.5 py-0.5 rounded-full bg-rose-500 text-white text-[10px]">@_favourites.Count</span>
|
| 45 |
+
}
|
| 46 |
+
</span>
|
| 47 |
+
</button>
|
| 48 |
+
</nav>
|
| 49 |
+
</div>
|
| 50 |
|
| 51 |
+
@if (_isLoading)
|
| 52 |
+
{
|
| 53 |
+
<div class="py-20 flex justify-center">
|
| 54 |
+
<div class="animate-spin rounded-full h-10 w-10 border-b-2 border-mystic-900"></div>
|
| 55 |
+
</div>
|
| 56 |
+
}
|
| 57 |
+
else
|
|
|
|
|
|
|
|
|
|
| 58 |
{
|
| 59 |
+
<div class="animate-in fade-in duration-300">
|
| 60 |
+
@if (_activeTab == "wishlist")
|
| 61 |
+
{
|
| 62 |
+
@if (!_wishlist.Any())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
{
|
| 64 |
+
<div class="py-20 text-center bg-mystic-50 rounded-3xl border-2 border-dashed border-mystic-200">
|
| 65 |
+
<p class="text-mystic-400 font-medium">Your wishlist is empty. Add books that are currently out of stock!</p>
|
| 66 |
+
<a href="Books" class="inline-block mt-4 text-sm font-bold text-mystic-900 hover:underline underline-offset-4">Browse Catalog →</a>
|
| 67 |
+
</div>
|
| 68 |
+
}
|
| 69 |
+
else
|
| 70 |
+
{
|
| 71 |
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 72 |
+
@foreach (var book in _wishlist)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
{
|
| 74 |
+
<div class="card p-4 flex gap-4 group hover:shadow-md transition-shadow relative">
|
| 75 |
+
<div class="w-20 h-28 bg-mystic-100 rounded-lg flex-shrink-0 flex items-center justify-center text-mystic-300 overflow-hidden border border-mystic-100">
|
| 76 |
+
@if (!string.IsNullOrWhiteSpace(book.CoverUrl))
|
| 77 |
+
{
|
| 78 |
+
<img src="@book.CoverUrl" alt="@book.Title" class="w-full h-full object-cover" />
|
| 79 |
+
}
|
| 80 |
+
else
|
| 81 |
+
{
|
| 82 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
|
| 83 |
+
}
|
| 84 |
+
</div>
|
| 85 |
+
<div class="flex-grow min-w-0">
|
| 86 |
+
<h3 class="text-sm font-bold text-mystic-900 truncate">@book.Title</h3>
|
| 87 |
+
<p class="text-xs text-mystic-500 mt-0.5 truncate">@book.Author</p>
|
| 88 |
+
<div class="mt-2">
|
| 89 |
+
<span class="inline-flex items-center rounded-full @(book.AvailableCopies > 0 ? "bg-emerald-50 text-emerald-700" : "bg-rose-50 text-rose-700") px-2 py-0.5 text-[10px] font-bold">
|
| 90 |
+
@(book.AvailableCopies > 0 ? "Now Available" : "Out of Stock")
|
| 91 |
+
</span>
|
| 92 |
+
</div>
|
| 93 |
+
<div class="mt-4 flex items-center gap-2">
|
| 94 |
+
<button @onclick='() => Navigation.NavigateTo($"/Books")' class="text-[10px] font-bold text-mystic-600 hover:text-mystic-900 uppercase tracking-tight">View Info</button>
|
| 95 |
+
<span class="text-mystic-200">|</span>
|
| 96 |
+
<button @onclick="() => RemoveFromWishlist(book.Id)" class="text-[10px] font-bold text-rose-600 hover:text-rose-700 uppercase tracking-tight">Remove</button>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
@if (book.AvailableCopies > 0)
|
| 100 |
+
{
|
| 101 |
+
<div class="absolute -top-2 -right-2 bg-emerald-500 text-white p-1 rounded-full shadow-sm animate-bounce">
|
| 102 |
+
<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="M5 13l4 4L19 7"></path></svg>
|
| 103 |
+
</div>
|
| 104 |
+
}
|
| 105 |
</div>
|
| 106 |
}
|
| 107 |
</div>
|
| 108 |
+
<div class="mt-8 p-4 bg-mystic-900 rounded-2xl text-white flex items-center gap-4">
|
| 109 |
+
<div class="p-2 bg-mystic-800 rounded-lg">
|
| 110 |
+
<svg class="w-6 h-6 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
| 111 |
+
</div>
|
| 112 |
+
<p class="text-xs font-medium">We'll notify you via Firebase when out-of-stock items become available!</p>
|
| 113 |
+
</div>
|
| 114 |
}
|
| 115 |
+
}
|
| 116 |
+
else
|
| 117 |
+
{
|
| 118 |
+
@if (!_favourites.Any())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
{
|
| 120 |
+
<div class="py-20 text-center bg-mystic-50 rounded-3xl border-2 border-dashed border-mystic-200">
|
| 121 |
+
<p class="text-mystic-400 font-medium">You haven't added any favourites yet.</p>
|
| 122 |
+
<a href="Books" class="inline-block mt-4 text-sm font-bold text-mystic-900 hover:underline underline-offset-4">Browse Catalog →</a>
|
| 123 |
+
</div>
|
| 124 |
+
}
|
| 125 |
+
else
|
| 126 |
+
{
|
| 127 |
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 128 |
+
@foreach (var book in _favourites)
|
| 129 |
+
{
|
| 130 |
+
<div class="card p-4 flex gap-4 group hover:shadow-md transition-shadow">
|
| 131 |
+
<div class="w-20 h-28 bg-mystic-100 rounded-lg flex-shrink-0 flex items-center justify-center text-mystic-300 overflow-hidden border border-mystic-100">
|
| 132 |
+
@if (!string.IsNullOrWhiteSpace(book.CoverUrl))
|
| 133 |
+
{
|
| 134 |
+
<img src="@book.CoverUrl" alt="@book.Title" class="w-full h-full object-cover" />
|
| 135 |
+
}
|
| 136 |
+
else
|
| 137 |
+
{
|
| 138 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
|
| 139 |
+
}
|
| 140 |
+
</div>
|
| 141 |
+
<div class="flex-grow min-w-0">
|
| 142 |
+
<h3 class="text-sm font-bold text-mystic-900 truncate">@book.Title</h3>
|
| 143 |
+
<p class="text-xs text-mystic-500 mt-0.5 truncate">@book.Author</p>
|
| 144 |
+
<div class="mt-4 flex items-center gap-2">
|
| 145 |
+
<button @onclick='() => Navigation.NavigateTo($"/Books")' class="text-[10px] font-bold text-mystic-600 hover:text-mystic-900 uppercase tracking-tight">View Info</button>
|
| 146 |
+
<span class="text-mystic-200">|</span>
|
| 147 |
+
<button @onclick="() => RemoveFromFavourite(book.Id)" class="text-[10px] font-bold text-rose-600 hover:text-rose-700 uppercase tracking-tight">Remove</button>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
</div>
|
| 151 |
+
}
|
| 152 |
</div>
|
| 153 |
}
|
| 154 |
+
}
|
| 155 |
+
</div>
|
| 156 |
}
|
| 157 |
+
</Authorized>
|
| 158 |
+
<NotAuthorized>
|
| 159 |
+
<div class="py-20 text-center">
|
| 160 |
+
<p class="text-mystic-500">Please log in as a member to view your wishlist.</p>
|
| 161 |
+
</div>
|
| 162 |
+
</NotAuthorized>
|
| 163 |
+
</AuthorizeView>
|
| 164 |
|
| 165 |
@code {
|
| 166 |
private string _activeTab = "wishlist";
|
|
|
|
| 182 |
{
|
| 183 |
_wishlist = await WishlistService.GetWishlistAsync();
|
| 184 |
_favourites = await WishlistService.GetFavouritesAsync();
|
| 185 |
+
|
| 186 |
+
// Ensure server is synced with current local storage
|
| 187 |
+
await WishlistService.SyncCurrentWishlistAsync();
|
| 188 |
}
|
| 189 |
|
| 190 |
private async Task RemoveFromWishlist(int id)
|
BlazorFrontend/Models/Dtos/LibraryDtos.cs
CHANGED
|
@@ -285,4 +285,16 @@ namespace BlazorFrontend.Models.Dtos
|
|
| 285 |
[JsonPropertyName("redemptions")]
|
| 286 |
public IEnumerable<LoyaltyRedemptionDto> Redemptions { get; set; } = Enumerable.Empty<LoyaltyRedemptionDto>();
|
| 287 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
}
|
|
|
|
| 285 |
[JsonPropertyName("redemptions")]
|
| 286 |
public IEnumerable<LoyaltyRedemptionDto> Redemptions { get; set; } = Enumerable.Empty<LoyaltyRedemptionDto>();
|
| 287 |
}
|
| 288 |
+
|
| 289 |
+
public class NotificationDto
|
| 290 |
+
{
|
| 291 |
+
public int Id { get; set; }
|
| 292 |
+
public string Title { get; set; } = string.Empty;
|
| 293 |
+
public string Message { get; set; } = string.Empty;
|
| 294 |
+
public string Type { get; set; } = "Info";
|
| 295 |
+
public DateTime CreatedAt { get; set; }
|
| 296 |
+
public bool IsRead { get; set; }
|
| 297 |
+
public string? ActionLink { get; set; }
|
| 298 |
+
public string? ActionText { get; set; }
|
| 299 |
+
}
|
| 300 |
}
|
BlazorFrontend/Services/LibraryApiClient.cs
CHANGED
|
@@ -47,6 +47,13 @@ namespace BlazorFrontend.Services
|
|
| 47 |
return await _httpClient.GetFromJsonAsync<BookDto>($"api/books/{id}");
|
| 48 |
}
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
public async Task<BookDto?> CreateBookAsync(BookCreateRequest request)
|
| 51 |
{
|
| 52 |
var response = await _httpClient.PostAsJsonAsync("api/books", request);
|
|
@@ -371,5 +378,38 @@ namespace BlazorFrontend.Services
|
|
| 371 |
}
|
| 372 |
catch (Exception ex) { return (false, $"Error: {ex.Message}"); }
|
| 373 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
}
|
| 375 |
}
|
|
|
|
| 47 |
return await _httpClient.GetFromJsonAsync<BookDto>($"api/books/{id}");
|
| 48 |
}
|
| 49 |
|
| 50 |
+
public async Task<List<BookDto>> GetBooksByIdsAsync(List<int> ids)
|
| 51 |
+
{
|
| 52 |
+
if (ids == null || !ids.Any()) return new List<BookDto>();
|
| 53 |
+
var idsStr = string.Join(",", ids);
|
| 54 |
+
return await _httpClient.GetFromJsonAsync<List<BookDto>>($"api/books/by-ids?ids={idsStr}") ?? new List<BookDto>();
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
public async Task<BookDto?> CreateBookAsync(BookCreateRequest request)
|
| 58 |
{
|
| 59 |
var response = await _httpClient.PostAsJsonAsync("api/books", request);
|
|
|
|
| 378 |
}
|
| 379 |
catch (Exception ex) { return (false, $"Error: {ex.Message}"); }
|
| 380 |
}
|
| 381 |
+
#region Notifications
|
| 382 |
+
public async Task<IEnumerable<NotificationDto>> GetNotificationsAsync()
|
| 383 |
+
{
|
| 384 |
+
return await _httpClient.GetFromJsonAsync<IEnumerable<NotificationDto>>("api/notifications") ?? Enumerable.Empty<NotificationDto>();
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
public async Task<int> GetUnreadNotificationCountAsync()
|
| 388 |
+
{
|
| 389 |
+
try
|
| 390 |
+
{
|
| 391 |
+
return await _httpClient.GetFromJsonAsync<int>("api/notifications/unread-count");
|
| 392 |
+
}
|
| 393 |
+
catch { return 0; }
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
public async Task<bool> MarkNotificationsReadAsync()
|
| 397 |
+
{
|
| 398 |
+
var response = await _httpClient.PostAsJsonAsync("api/notifications/mark-read", new { });
|
| 399 |
+
return response.IsSuccessStatusCode;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
public async Task<bool> UpdateFcmTokenAsync(string fcmToken)
|
| 403 |
+
{
|
| 404 |
+
var response = await _httpClient.PostAsJsonAsync("api/users/fcm-token", new { fcmToken });
|
| 405 |
+
return response.IsSuccessStatusCode;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
public async Task<bool> SyncWishlistAsync(List<int> bookIds)
|
| 409 |
+
{
|
| 410 |
+
var response = await _httpClient.PostAsJsonAsync("api/wishlist/sync", bookIds);
|
| 411 |
+
return response.IsSuccessStatusCode;
|
| 412 |
+
}
|
| 413 |
+
#endregion
|
| 414 |
}
|
| 415 |
}
|
BlazorFrontend/Services/WishlistService.cs
CHANGED
|
@@ -7,12 +7,30 @@ namespace BlazorFrontend.Services;
|
|
| 7 |
public class WishlistService
|
| 8 |
{
|
| 9 |
private readonly ProtectedLocalStorage _localStorage;
|
|
|
|
| 10 |
private const string WishlistKey = "user_wishlist";
|
| 11 |
private const string FavouriteKey = "user_favourites";
|
| 12 |
|
| 13 |
-
public WishlistService(ProtectedLocalStorage localStorage)
|
| 14 |
{
|
| 15 |
_localStorage = localStorage;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
}
|
| 17 |
|
| 18 |
public async Task<List<BookDto>> GetWishlistAsync()
|
|
@@ -22,7 +40,18 @@ public class WishlistService
|
|
| 22 |
var result = await _localStorage.GetAsync<string>(WishlistKey);
|
| 23 |
if (result.Success && !string.IsNullOrWhiteSpace(result.Value))
|
| 24 |
{
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
}
|
| 28 |
catch { }
|
|
@@ -36,6 +65,7 @@ public class WishlistService
|
|
| 36 |
{
|
| 37 |
list.Add(book);
|
| 38 |
await _localStorage.SetAsync(WishlistKey, JsonSerializer.Serialize(list));
|
|
|
|
| 39 |
}
|
| 40 |
}
|
| 41 |
|
|
@@ -47,6 +77,7 @@ public class WishlistService
|
|
| 47 |
{
|
| 48 |
list.Remove(item);
|
| 49 |
await _localStorage.SetAsync(WishlistKey, JsonSerializer.Serialize(list));
|
|
|
|
| 50 |
}
|
| 51 |
}
|
| 52 |
|
|
@@ -57,7 +88,18 @@ public class WishlistService
|
|
| 57 |
var result = await _localStorage.GetAsync<string>(FavouriteKey);
|
| 58 |
if (result.Success && !string.IsNullOrWhiteSpace(result.Value))
|
| 59 |
{
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
}
|
| 62 |
}
|
| 63 |
catch { }
|
|
|
|
| 7 |
public class WishlistService
|
| 8 |
{
|
| 9 |
private readonly ProtectedLocalStorage _localStorage;
|
| 10 |
+
private readonly LibraryApiClient _apiClient;
|
| 11 |
private const string WishlistKey = "user_wishlist";
|
| 12 |
private const string FavouriteKey = "user_favourites";
|
| 13 |
|
| 14 |
+
public WishlistService(ProtectedLocalStorage localStorage, LibraryApiClient apiClient)
|
| 15 |
{
|
| 16 |
_localStorage = localStorage;
|
| 17 |
+
_apiClient = apiClient;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
public async Task SyncCurrentWishlistAsync()
|
| 21 |
+
{
|
| 22 |
+
var list = await GetWishlistAsync();
|
| 23 |
+
await SyncWithServerAsync(list);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
private async Task SyncWithServerAsync(List<BookDto> list)
|
| 27 |
+
{
|
| 28 |
+
try
|
| 29 |
+
{
|
| 30 |
+
var bookIds = list.Select(b => b.Id).ToList();
|
| 31 |
+
await _apiClient.SyncWishlistAsync(bookIds);
|
| 32 |
+
}
|
| 33 |
+
catch { }
|
| 34 |
}
|
| 35 |
|
| 36 |
public async Task<List<BookDto>> GetWishlistAsync()
|
|
|
|
| 40 |
var result = await _localStorage.GetAsync<string>(WishlistKey);
|
| 41 |
if (result.Success && !string.IsNullOrWhiteSpace(result.Value))
|
| 42 |
{
|
| 43 |
+
var localList = JsonSerializer.Deserialize<List<BookDto>>(result.Value) ?? new List<BookDto>();
|
| 44 |
+
if (!localList.Any()) return localList;
|
| 45 |
+
|
| 46 |
+
// Refresh from server
|
| 47 |
+
var latestList = await _apiClient.GetBooksByIdsAsync(localList.Select(b => b.Id).ToList());
|
| 48 |
+
if (latestList.Any())
|
| 49 |
+
{
|
| 50 |
+
// Update local storage with fresh data
|
| 51 |
+
await _localStorage.SetAsync(WishlistKey, JsonSerializer.Serialize(latestList));
|
| 52 |
+
return latestList;
|
| 53 |
+
}
|
| 54 |
+
return localList;
|
| 55 |
}
|
| 56 |
}
|
| 57 |
catch { }
|
|
|
|
| 65 |
{
|
| 66 |
list.Add(book);
|
| 67 |
await _localStorage.SetAsync(WishlistKey, JsonSerializer.Serialize(list));
|
| 68 |
+
await SyncWithServerAsync(list);
|
| 69 |
}
|
| 70 |
}
|
| 71 |
|
|
|
|
| 77 |
{
|
| 78 |
list.Remove(item);
|
| 79 |
await _localStorage.SetAsync(WishlistKey, JsonSerializer.Serialize(list));
|
| 80 |
+
await SyncWithServerAsync(list);
|
| 81 |
}
|
| 82 |
}
|
| 83 |
|
|
|
|
| 88 |
var result = await _localStorage.GetAsync<string>(FavouriteKey);
|
| 89 |
if (result.Success && !string.IsNullOrWhiteSpace(result.Value))
|
| 90 |
{
|
| 91 |
+
var localList = JsonSerializer.Deserialize<List<BookDto>>(result.Value) ?? new List<BookDto>();
|
| 92 |
+
if (!localList.Any()) return localList;
|
| 93 |
+
|
| 94 |
+
// Refresh from server
|
| 95 |
+
var latestList = await _apiClient.GetBooksByIdsAsync(localList.Select(b => b.Id).ToList());
|
| 96 |
+
if (latestList.Any())
|
| 97 |
+
{
|
| 98 |
+
// Update local storage with fresh data
|
| 99 |
+
await _localStorage.SetAsync(FavouriteKey, JsonSerializer.Serialize(latestList));
|
| 100 |
+
return latestList;
|
| 101 |
+
}
|
| 102 |
+
return localList;
|
| 103 |
}
|
| 104 |
}
|
| 105 |
catch { }
|
BlazorFrontend/wwwroot/firebase-init.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
|
| 2 |
+
import { getMessaging, getToken, onMessage } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-messaging.js";
|
| 3 |
+
|
| 4 |
+
const firebaseConfig = {
|
| 5 |
+
apiKey: "AIzaSyBodr7GJuCFdJfBYV8XBY77P9A4Wrxz_CA",
|
| 6 |
+
authDomain: "library-ths.firebaseapp.com",
|
| 7 |
+
projectId: "library-ths",
|
| 8 |
+
storageBucket: "library-ths.firebasestorage.app",
|
| 9 |
+
messagingSenderId: "222408896403",
|
| 10 |
+
appId: "1:222408896403:web:8533e26175c043e40a0266",
|
| 11 |
+
measurementId: "G-LVVGH9T1K3"
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
const app = initializeApp(firebaseConfig);
|
| 15 |
+
const messaging = getMessaging(app);
|
| 16 |
+
|
| 17 |
+
window.getFcmToken = async () => {
|
| 18 |
+
try {
|
| 19 |
+
const permission = await Notification.requestPermission();
|
| 20 |
+
if (permission === 'granted') {
|
| 21 |
+
// IMPORTANT: Replace 'YOUR_VAPID_KEY' with your actual key from
|
| 22 |
+
// Firebase Console -> Project Settings -> Cloud Messaging -> Web configuration -> Web Push certificates
|
| 23 |
+
console.log('Registering service worker...');
|
| 24 |
+
const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js');
|
| 25 |
+
console.log('Service worker registered successfully:', registration.scope);
|
| 26 |
+
|
| 27 |
+
const token = await getToken(messaging, {
|
| 28 |
+
vapidKey: 'BNgDM2Eq7TPwap0FhF-xv0CK6cS5EfshHOiC8z4916XnewFWYKdAfhwHctsgte5q8jQicQO8PEDVeymlXToasVQ',
|
| 29 |
+
serviceWorkerRegistration: registration
|
| 30 |
+
});
|
| 31 |
+
return token;
|
| 32 |
+
} else {
|
| 33 |
+
throw new Error('Permission not granted for notifications. Current state: ' + permission);
|
| 34 |
+
}
|
| 35 |
+
} catch (error) {
|
| 36 |
+
console.error('Error getting FCM token:', error);
|
| 37 |
+
throw new Error(error.message || error.toString());
|
| 38 |
+
}
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
onMessage(messaging, (payload) => {
|
| 42 |
+
console.log('Foreground Message received: ', payload);
|
| 43 |
+
|
| 44 |
+
const title = payload.notification.title;
|
| 45 |
+
const body = payload.notification.body;
|
| 46 |
+
|
| 47 |
+
if (window.showToast) {
|
| 48 |
+
window.showToast(title + ": " + body, 'success');
|
| 49 |
+
}
|
| 50 |
+
});
|
BlazorFrontend/wwwroot/firebase-messaging-sw.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Scripts for firebase and messaging
|
| 2 |
+
// Service worker တွေမှာ import အစား importScripts ကို သုံးပါတယ်
|
| 3 |
+
importScripts('https://www.gstatic.com/firebasejs/11.6.1/firebase-app-compat.js');
|
| 4 |
+
importScripts('https://www.gstatic.com/firebasejs/11.6.1/firebase-messaging-compat.js');
|
| 5 |
+
|
| 6 |
+
// --- Firebase Config ---
|
| 7 |
+
// Step 2 ကနေ ရခဲ့တဲ့ ကျွန်တော်တို့ရဲ့ Firebase Project Configuration ကို ဒီနေရာမှာ ထည့်ပေးပါ
|
| 8 |
+
const firebaseConfig = {
|
| 9 |
+
apiKey: "AIzaSyBodr7GJuCFdJfBYV8XBY77P9A4Wrxz_CA",
|
| 10 |
+
authDomain: "library-ths.firebaseapp.com",
|
| 11 |
+
projectId: "library-ths",
|
| 12 |
+
storageBucket: "library-ths.firebasestorage.app",
|
| 13 |
+
messagingSenderId: "222408896403",
|
| 14 |
+
appId: "1:222408896403:web:8533e26175c043e40a0266",
|
| 15 |
+
measurementId: "G-LVVGH9T1K3"
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
// Service worker မှာ Firebase app ကို initialize လုပ်ခြင်း
|
| 19 |
+
firebase.initializeApp(firebaseConfig);
|
| 20 |
+
|
| 21 |
+
// Background မှာ message တွေ လက်ခံနိုင်ဖို့ Firebase Messaging instance ကို ရယူခြင်း
|
| 22 |
+
const messaging = firebase.messaging();
|
| 23 |
+
|
| 24 |
+
// Background မှာရောက်လာတဲ့ message တွေကို ကိုင်တွယ်ဖြေရှင်းမယ့် အပိုင်း
|
| 25 |
+
messaging.onBackgroundMessage(function(payload) {
|
| 26 |
+
console.log('[firebase-messaging-sw.js] Received background message ', payload);
|
| 27 |
+
|
| 28 |
+
const title = payload.notification.title;
|
| 29 |
+
const body = payload.notification.body;
|
| 30 |
+
|
| 31 |
+
const notificationOptions = {
|
| 32 |
+
body: body,
|
| 33 |
+
icon: '/favicon.png'
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
self.registration.showNotification(title, notificationOptions);
|
| 37 |
+
});
|
DbConnect/Data/AppDbContext.cs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 2 |
using System.Collections.Generic;
|
| 3 |
using DbConnect.Entities;
|
| 4 |
using Microsoft.EntityFrameworkCore;
|
|
@@ -28,10 +28,11 @@ public partial class AppDbContext : DbContext
|
|
| 28 |
|
| 29 |
public virtual DbSet<UserSubscription> UserSubscriptions { get; set; }
|
| 30 |
|
| 31 |
-
|
| 32 |
-
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
|
| 33 |
-
=> optionsBuilder.UseSqlServer("Server=.;Database=LibraryManagement;User Id=sa;Password=sasa@123;TrustServerCertificate=True;");
|
| 34 |
|
|
|
|
|
|
|
|
|
|
| 35 |
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
| 36 |
{
|
| 37 |
modelBuilder.Entity<Book>(entity =>
|
|
@@ -135,6 +136,7 @@ public partial class AppDbContext : DbContext
|
|
| 135 |
entity.Property(e => e.StudentId).HasMaxLength(20);
|
| 136 |
entity.Property(e => e.SuspensionEndDate).HasColumnType("datetime");
|
| 137 |
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
|
|
|
| 138 |
});
|
| 139 |
|
| 140 |
modelBuilder.Entity<UserSubscription>(entity =>
|
|
@@ -159,6 +161,45 @@ public partial class AppDbContext : DbContext
|
|
| 159 |
.HasConstraintName("FK_UserSubscriptions_Users");
|
| 160 |
});
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
OnModelCreatingPartial(modelBuilder);
|
| 163 |
}
|
| 164 |
|
|
|
|
| 1 |
+
using System;
|
| 2 |
using System.Collections.Generic;
|
| 3 |
using DbConnect.Entities;
|
| 4 |
using Microsoft.EntityFrameworkCore;
|
|
|
|
| 28 |
|
| 29 |
public virtual DbSet<UserSubscription> UserSubscriptions { get; set; }
|
| 30 |
|
| 31 |
+
public virtual DbSet<Notification> Notifications { get; set; }
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
public virtual DbSet<WishlistItem> WishlistItems { get; set; }
|
| 34 |
+
|
| 35 |
+
|
| 36 |
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
| 37 |
{
|
| 38 |
modelBuilder.Entity<Book>(entity =>
|
|
|
|
| 136 |
entity.Property(e => e.StudentId).HasMaxLength(20);
|
| 137 |
entity.Property(e => e.SuspensionEndDate).HasColumnType("datetime");
|
| 138 |
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
| 139 |
+
entity.Property(e => e.FcmToken).HasMaxLength(500);
|
| 140 |
});
|
| 141 |
|
| 142 |
modelBuilder.Entity<UserSubscription>(entity =>
|
|
|
|
| 161 |
.HasConstraintName("FK_UserSubscriptions_Users");
|
| 162 |
});
|
| 163 |
|
| 164 |
+
modelBuilder.Entity<Notification>(entity =>
|
| 165 |
+
{
|
| 166 |
+
entity.HasKey(e => e.Id);
|
| 167 |
+
|
| 168 |
+
entity.Property(e => e.Title).HasMaxLength(200);
|
| 169 |
+
entity.Property(e => e.Message).HasMaxLength(1000);
|
| 170 |
+
entity.Property(e => e.Type).HasMaxLength(50).HasDefaultValue("Info");
|
| 171 |
+
entity.Property(e => e.ActionLink).HasMaxLength(500);
|
| 172 |
+
entity.Property(e => e.ActionText).HasMaxLength(100);
|
| 173 |
+
entity.Property(e => e.IsRead).HasDefaultValue(false);
|
| 174 |
+
entity.Property(e => e.CreatedAt)
|
| 175 |
+
.HasDefaultValueSql("(getdate())")
|
| 176 |
+
.HasColumnType("datetime");
|
| 177 |
+
|
| 178 |
+
entity.HasOne(d => d.User).WithMany()
|
| 179 |
+
.HasForeignKey(d => d.UserId)
|
| 180 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 181 |
+
.HasConstraintName("FK_Notifications_Users");
|
| 182 |
+
});
|
| 183 |
+
|
| 184 |
+
modelBuilder.Entity<WishlistItem>(entity =>
|
| 185 |
+
{
|
| 186 |
+
entity.HasKey(e => e.Id);
|
| 187 |
+
|
| 188 |
+
entity.Property(e => e.AddedAt)
|
| 189 |
+
.HasDefaultValueSql("(getdate())")
|
| 190 |
+
.HasColumnType("datetime");
|
| 191 |
+
|
| 192 |
+
entity.HasOne(d => d.Book).WithMany()
|
| 193 |
+
.HasForeignKey(d => d.BookId)
|
| 194 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 195 |
+
.HasConstraintName("FK_WishlistItems_Books");
|
| 196 |
+
|
| 197 |
+
entity.HasOne(d => d.User).WithMany()
|
| 198 |
+
.HasForeignKey(d => d.UserId)
|
| 199 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 200 |
+
.HasConstraintName("FK_WishlistItems_Users");
|
| 201 |
+
});
|
| 202 |
+
|
| 203 |
OnModelCreatingPartial(modelBuilder);
|
| 204 |
}
|
| 205 |
|
DbConnect/Entities/Notification.cs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using System;
|
| 2 |
+
|
| 3 |
+
namespace DbConnect.Entities;
|
| 4 |
+
|
| 5 |
+
public partial class Notification
|
| 6 |
+
{
|
| 7 |
+
public int Id { get; set; }
|
| 8 |
+
|
| 9 |
+
public Guid UserId { get; set; }
|
| 10 |
+
|
| 11 |
+
public string Title { get; set; } = null!;
|
| 12 |
+
|
| 13 |
+
public string Message { get; set; } = null!;
|
| 14 |
+
|
| 15 |
+
public string Type { get; set; } = "Info";
|
| 16 |
+
|
| 17 |
+
public string? ActionLink { get; set; }
|
| 18 |
+
|
| 19 |
+
public string? ActionText { get; set; }
|
| 20 |
+
|
| 21 |
+
public bool IsRead { get; set; }
|
| 22 |
+
|
| 23 |
+
public DateTime CreatedAt { get; set; }
|
| 24 |
+
|
| 25 |
+
public virtual User User { get; set; } = null!;
|
| 26 |
+
}
|
DbConnect/Entities/User.cs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 2 |
using System.Collections.Generic;
|
| 3 |
|
| 4 |
namespace DbConnect.Entities;
|
|
@@ -31,6 +31,8 @@ public partial class User
|
|
| 31 |
|
| 32 |
public string? Address { get; set; }
|
| 33 |
|
|
|
|
|
|
|
| 34 |
public virtual ICollection<Borrowing> Borrowings { get; set; } = new List<Borrowing>();
|
| 35 |
|
| 36 |
public virtual ICollection<UserSubscription> UserSubscriptions { get; set; } = new List<UserSubscription>();
|
|
|
|
| 1 |
+
using System;
|
| 2 |
using System.Collections.Generic;
|
| 3 |
|
| 4 |
namespace DbConnect.Entities;
|
|
|
|
| 31 |
|
| 32 |
public string? Address { get; set; }
|
| 33 |
|
| 34 |
+
public string? FcmToken { get; set; }
|
| 35 |
+
|
| 36 |
public virtual ICollection<Borrowing> Borrowings { get; set; } = new List<Borrowing>();
|
| 37 |
|
| 38 |
public virtual ICollection<UserSubscription> UserSubscriptions { get; set; } = new List<UserSubscription>();
|
DbConnect/Entities/WishlistItem.cs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using System;
|
| 2 |
+
|
| 3 |
+
namespace DbConnect.Entities;
|
| 4 |
+
|
| 5 |
+
public partial class WishlistItem
|
| 6 |
+
{
|
| 7 |
+
public int Id { get; set; }
|
| 8 |
+
|
| 9 |
+
public Guid UserId { get; set; }
|
| 10 |
+
|
| 11 |
+
public int BookId { get; set; }
|
| 12 |
+
|
| 13 |
+
public DateTime AddedAt { get; set; }
|
| 14 |
+
|
| 15 |
+
public virtual Book Book { get; set; } = null!;
|
| 16 |
+
|
| 17 |
+
public virtual User User { get; set; } = null!;
|
| 18 |
+
}
|
DbConnect/Migrations/20260427043736_AddNotifications.Designer.cs
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// <auto-generated />
|
| 2 |
+
using System;
|
| 3 |
+
using DbConnect.Data;
|
| 4 |
+
using Microsoft.EntityFrameworkCore;
|
| 5 |
+
using Microsoft.EntityFrameworkCore.Infrastructure;
|
| 6 |
+
using Microsoft.EntityFrameworkCore.Metadata;
|
| 7 |
+
using Microsoft.EntityFrameworkCore.Migrations;
|
| 8 |
+
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
| 9 |
+
|
| 10 |
+
#nullable disable
|
| 11 |
+
|
| 12 |
+
namespace DbConnect.Migrations
|
| 13 |
+
{
|
| 14 |
+
[DbContext(typeof(AppDbContext))]
|
| 15 |
+
[Migration("20260427043736_AddNotifications")]
|
| 16 |
+
partial class AddNotifications
|
| 17 |
+
{
|
| 18 |
+
/// <inheritdoc />
|
| 19 |
+
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
| 20 |
+
{
|
| 21 |
+
#pragma warning disable 612, 618
|
| 22 |
+
modelBuilder
|
| 23 |
+
.HasAnnotation("ProductVersion", "9.0.0")
|
| 24 |
+
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
| 25 |
+
|
| 26 |
+
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
| 27 |
+
|
| 28 |
+
modelBuilder.Entity("BookCategory", b =>
|
| 29 |
+
{
|
| 30 |
+
b.Property<int>("BookId")
|
| 31 |
+
.HasColumnType("int");
|
| 32 |
+
|
| 33 |
+
b.Property<int>("CategoryId")
|
| 34 |
+
.HasColumnType("int");
|
| 35 |
+
|
| 36 |
+
b.HasKey("BookId", "CategoryId")
|
| 37 |
+
.HasName("PK__BookCate__9C7051A74E7D221D");
|
| 38 |
+
|
| 39 |
+
b.HasIndex("CategoryId");
|
| 40 |
+
|
| 41 |
+
b.ToTable("BookCategories", (string)null);
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
modelBuilder.Entity("DbConnect.Entities.Book", b =>
|
| 45 |
+
{
|
| 46 |
+
b.Property<int>("Id")
|
| 47 |
+
.ValueGeneratedOnAdd()
|
| 48 |
+
.HasColumnType("int");
|
| 49 |
+
|
| 50 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 51 |
+
|
| 52 |
+
b.Property<string>("Author")
|
| 53 |
+
.IsRequired()
|
| 54 |
+
.HasMaxLength(100)
|
| 55 |
+
.HasColumnType("nvarchar(100)");
|
| 56 |
+
|
| 57 |
+
b.Property<int>("AvailableCopies")
|
| 58 |
+
.ValueGeneratedOnAdd()
|
| 59 |
+
.HasColumnType("int")
|
| 60 |
+
.HasDefaultValue(1);
|
| 61 |
+
|
| 62 |
+
b.Property<string>("CoverUrl")
|
| 63 |
+
.HasColumnType("nvarchar(max)");
|
| 64 |
+
|
| 65 |
+
b.Property<DateTime>("CreatedAt")
|
| 66 |
+
.ValueGeneratedOnAdd()
|
| 67 |
+
.HasColumnType("datetime")
|
| 68 |
+
.HasDefaultValueSql("(getdate())");
|
| 69 |
+
|
| 70 |
+
b.Property<string>("Description")
|
| 71 |
+
.HasColumnType("nvarchar(max)");
|
| 72 |
+
|
| 73 |
+
b.Property<bool>("IsActive")
|
| 74 |
+
.ValueGeneratedOnAdd()
|
| 75 |
+
.HasColumnType("bit")
|
| 76 |
+
.HasDefaultValue(true);
|
| 77 |
+
|
| 78 |
+
b.Property<string>("Isbn")
|
| 79 |
+
.IsRequired()
|
| 80 |
+
.HasMaxLength(20)
|
| 81 |
+
.HasColumnType("nvarchar(20)")
|
| 82 |
+
.HasColumnName("ISBN");
|
| 83 |
+
|
| 84 |
+
b.Property<string>("Status")
|
| 85 |
+
.IsRequired()
|
| 86 |
+
.ValueGeneratedOnAdd()
|
| 87 |
+
.HasMaxLength(20)
|
| 88 |
+
.HasColumnType("nvarchar(20)")
|
| 89 |
+
.HasDefaultValue("Available");
|
| 90 |
+
|
| 91 |
+
b.Property<string>("Title")
|
| 92 |
+
.IsRequired()
|
| 93 |
+
.HasMaxLength(200)
|
| 94 |
+
.HasColumnType("nvarchar(200)");
|
| 95 |
+
|
| 96 |
+
b.Property<int>("TotalCopies")
|
| 97 |
+
.ValueGeneratedOnAdd()
|
| 98 |
+
.HasColumnType("int")
|
| 99 |
+
.HasDefaultValue(1);
|
| 100 |
+
|
| 101 |
+
b.Property<DateTime?>("UpdatedAt")
|
| 102 |
+
.HasColumnType("datetime");
|
| 103 |
+
|
| 104 |
+
b.HasKey("Id")
|
| 105 |
+
.HasName("PK__Books__3214EC07AE1974C8");
|
| 106 |
+
|
| 107 |
+
b.HasIndex(new[] { "Isbn" }, "UQ__Books__447D36EA79CF66CE")
|
| 108 |
+
.IsUnique();
|
| 109 |
+
|
| 110 |
+
b.ToTable("Books");
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
modelBuilder.Entity("DbConnect.Entities.Borrowing", b =>
|
| 114 |
+
{
|
| 115 |
+
b.Property<Guid>("Id")
|
| 116 |
+
.ValueGeneratedOnAdd()
|
| 117 |
+
.HasColumnType("uniqueidentifier")
|
| 118 |
+
.HasDefaultValueSql("(newid())");
|
| 119 |
+
|
| 120 |
+
b.Property<int>("BookId")
|
| 121 |
+
.HasColumnType("int");
|
| 122 |
+
|
| 123 |
+
b.Property<DateTime>("BorrowDate")
|
| 124 |
+
.ValueGeneratedOnAdd()
|
| 125 |
+
.HasColumnType("datetime")
|
| 126 |
+
.HasDefaultValueSql("(getdate())");
|
| 127 |
+
|
| 128 |
+
b.Property<DateTime>("DueDate")
|
| 129 |
+
.HasColumnType("datetime");
|
| 130 |
+
|
| 131 |
+
b.Property<decimal>("FineAmount")
|
| 132 |
+
.HasColumnType("decimal(10, 2)");
|
| 133 |
+
|
| 134 |
+
b.Property<bool>("IsFinePaid")
|
| 135 |
+
.HasColumnType("bit");
|
| 136 |
+
|
| 137 |
+
b.Property<DateTime?>("ReturnDate")
|
| 138 |
+
.HasColumnType("datetime");
|
| 139 |
+
|
| 140 |
+
b.Property<string>("Status")
|
| 141 |
+
.IsRequired()
|
| 142 |
+
.ValueGeneratedOnAdd()
|
| 143 |
+
.HasMaxLength(20)
|
| 144 |
+
.HasColumnType("nvarchar(20)")
|
| 145 |
+
.HasDefaultValue("Active");
|
| 146 |
+
|
| 147 |
+
b.Property<Guid>("UserId")
|
| 148 |
+
.HasColumnType("uniqueidentifier");
|
| 149 |
+
|
| 150 |
+
b.HasKey("Id");
|
| 151 |
+
|
| 152 |
+
b.HasIndex("BookId");
|
| 153 |
+
|
| 154 |
+
b.HasIndex("UserId");
|
| 155 |
+
|
| 156 |
+
b.ToTable("Borrowings");
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
+
modelBuilder.Entity("DbConnect.Entities.Category", b =>
|
| 160 |
+
{
|
| 161 |
+
b.Property<int>("Id")
|
| 162 |
+
.ValueGeneratedOnAdd()
|
| 163 |
+
.HasColumnType("int");
|
| 164 |
+
|
| 165 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 166 |
+
|
| 167 |
+
b.Property<string>("Name")
|
| 168 |
+
.IsRequired()
|
| 169 |
+
.HasMaxLength(100)
|
| 170 |
+
.HasColumnType("nvarchar(100)");
|
| 171 |
+
|
| 172 |
+
b.HasKey("Id")
|
| 173 |
+
.HasName("PK__Categori__3214EC0751B5797D");
|
| 174 |
+
|
| 175 |
+
b.HasIndex(new[] { "Name" }, "UQ__Categori__737584F622B16D3A")
|
| 176 |
+
.IsUnique();
|
| 177 |
+
|
| 178 |
+
b.ToTable("Categories");
|
| 179 |
+
});
|
| 180 |
+
|
| 181 |
+
modelBuilder.Entity("DbConnect.Entities.Membership", b =>
|
| 182 |
+
{
|
| 183 |
+
b.Property<int>("Id")
|
| 184 |
+
.ValueGeneratedOnAdd()
|
| 185 |
+
.HasColumnType("int");
|
| 186 |
+
|
| 187 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 188 |
+
|
| 189 |
+
b.Property<int>("BorrowingDays")
|
| 190 |
+
.HasColumnType("int");
|
| 191 |
+
|
| 192 |
+
b.Property<int>("DurationMonths")
|
| 193 |
+
.HasColumnType("int");
|
| 194 |
+
|
| 195 |
+
b.Property<int>("MaxBooks")
|
| 196 |
+
.HasColumnType("int");
|
| 197 |
+
|
| 198 |
+
b.Property<decimal>("Price")
|
| 199 |
+
.HasColumnType("decimal(10, 2)");
|
| 200 |
+
|
| 201 |
+
b.Property<string>("RewardId")
|
| 202 |
+
.HasMaxLength(100)
|
| 203 |
+
.HasColumnType("nvarchar(100)");
|
| 204 |
+
|
| 205 |
+
b.Property<string>("Type")
|
| 206 |
+
.IsRequired()
|
| 207 |
+
.HasMaxLength(50)
|
| 208 |
+
.HasColumnType("nvarchar(50)");
|
| 209 |
+
|
| 210 |
+
b.HasKey("Id")
|
| 211 |
+
.HasName("PK__Membersh__3214EC07A6F5A55F");
|
| 212 |
+
|
| 213 |
+
b.HasIndex(new[] { "Type" }, "IX_Memberships_Type")
|
| 214 |
+
.IsUnique();
|
| 215 |
+
|
| 216 |
+
b.ToTable("Memberships");
|
| 217 |
+
});
|
| 218 |
+
|
| 219 |
+
modelBuilder.Entity("DbConnect.Entities.Notification", b =>
|
| 220 |
+
{
|
| 221 |
+
b.Property<int>("Id")
|
| 222 |
+
.ValueGeneratedOnAdd()
|
| 223 |
+
.HasColumnType("int");
|
| 224 |
+
|
| 225 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 226 |
+
|
| 227 |
+
b.Property<string>("ActionLink")
|
| 228 |
+
.HasMaxLength(500)
|
| 229 |
+
.HasColumnType("nvarchar(500)");
|
| 230 |
+
|
| 231 |
+
b.Property<string>("ActionText")
|
| 232 |
+
.HasMaxLength(100)
|
| 233 |
+
.HasColumnType("nvarchar(100)");
|
| 234 |
+
|
| 235 |
+
b.Property<DateTime>("CreatedAt")
|
| 236 |
+
.ValueGeneratedOnAdd()
|
| 237 |
+
.HasColumnType("datetime")
|
| 238 |
+
.HasDefaultValueSql("(getdate())");
|
| 239 |
+
|
| 240 |
+
b.Property<bool>("IsRead")
|
| 241 |
+
.ValueGeneratedOnAdd()
|
| 242 |
+
.HasColumnType("bit")
|
| 243 |
+
.HasDefaultValue(false);
|
| 244 |
+
|
| 245 |
+
b.Property<string>("Message")
|
| 246 |
+
.IsRequired()
|
| 247 |
+
.HasMaxLength(1000)
|
| 248 |
+
.HasColumnType("nvarchar(1000)");
|
| 249 |
+
|
| 250 |
+
b.Property<string>("Title")
|
| 251 |
+
.IsRequired()
|
| 252 |
+
.HasMaxLength(200)
|
| 253 |
+
.HasColumnType("nvarchar(200)");
|
| 254 |
+
|
| 255 |
+
b.Property<string>("Type")
|
| 256 |
+
.IsRequired()
|
| 257 |
+
.ValueGeneratedOnAdd()
|
| 258 |
+
.HasMaxLength(50)
|
| 259 |
+
.HasColumnType("nvarchar(50)")
|
| 260 |
+
.HasDefaultValue("Info");
|
| 261 |
+
|
| 262 |
+
b.Property<Guid>("UserId")
|
| 263 |
+
.HasColumnType("uniqueidentifier");
|
| 264 |
+
|
| 265 |
+
b.HasKey("Id");
|
| 266 |
+
|
| 267 |
+
b.HasIndex("UserId");
|
| 268 |
+
|
| 269 |
+
b.ToTable("Notifications");
|
| 270 |
+
});
|
| 271 |
+
|
| 272 |
+
modelBuilder.Entity("DbConnect.Entities.User", b =>
|
| 273 |
+
{
|
| 274 |
+
b.Property<Guid>("Id")
|
| 275 |
+
.ValueGeneratedOnAdd()
|
| 276 |
+
.HasColumnType("uniqueidentifier")
|
| 277 |
+
.HasDefaultValueSql("(newid())");
|
| 278 |
+
|
| 279 |
+
b.Property<string>("Address")
|
| 280 |
+
.HasMaxLength(255)
|
| 281 |
+
.HasColumnType("nvarchar(255)");
|
| 282 |
+
|
| 283 |
+
b.Property<bool?>("BanStatus")
|
| 284 |
+
.HasColumnType("bit");
|
| 285 |
+
|
| 286 |
+
b.Property<DateTime>("CreatedAt")
|
| 287 |
+
.ValueGeneratedOnAdd()
|
| 288 |
+
.HasColumnType("datetime")
|
| 289 |
+
.HasDefaultValueSql("(getdate())");
|
| 290 |
+
|
| 291 |
+
b.Property<string>("Email")
|
| 292 |
+
.IsRequired()
|
| 293 |
+
.HasMaxLength(100)
|
| 294 |
+
.HasColumnType("nvarchar(100)");
|
| 295 |
+
|
| 296 |
+
b.Property<string>("FullName")
|
| 297 |
+
.IsRequired()
|
| 298 |
+
.HasMaxLength(100)
|
| 299 |
+
.HasColumnType("nvarchar(100)");
|
| 300 |
+
|
| 301 |
+
b.Property<bool>("IsActive")
|
| 302 |
+
.ValueGeneratedOnAdd()
|
| 303 |
+
.HasColumnType("bit")
|
| 304 |
+
.HasDefaultValue(true);
|
| 305 |
+
|
| 306 |
+
b.Property<string>("PasswordHash")
|
| 307 |
+
.IsRequired()
|
| 308 |
+
.HasColumnType("nvarchar(max)");
|
| 309 |
+
|
| 310 |
+
b.Property<string>("PhoneNumber")
|
| 311 |
+
.HasMaxLength(20)
|
| 312 |
+
.HasColumnType("nvarchar(20)");
|
| 313 |
+
|
| 314 |
+
b.Property<string>("Role")
|
| 315 |
+
.IsRequired()
|
| 316 |
+
.ValueGeneratedOnAdd()
|
| 317 |
+
.HasMaxLength(20)
|
| 318 |
+
.HasColumnType("nvarchar(20)")
|
| 319 |
+
.HasDefaultValue("User");
|
| 320 |
+
|
| 321 |
+
b.Property<string>("StudentId")
|
| 322 |
+
.HasMaxLength(20)
|
| 323 |
+
.HasColumnType("nvarchar(20)");
|
| 324 |
+
|
| 325 |
+
b.Property<DateTime?>("SuspensionEndDate")
|
| 326 |
+
.HasColumnType("datetime");
|
| 327 |
+
|
| 328 |
+
b.Property<DateTime?>("UpdatedAt")
|
| 329 |
+
.HasColumnType("datetime");
|
| 330 |
+
|
| 331 |
+
b.HasKey("Id");
|
| 332 |
+
|
| 333 |
+
b.HasIndex(new[] { "Email" }, "UQ_Users_Email")
|
| 334 |
+
.IsUnique();
|
| 335 |
+
|
| 336 |
+
b.ToTable("Users");
|
| 337 |
+
});
|
| 338 |
+
|
| 339 |
+
modelBuilder.Entity("DbConnect.Entities.UserSubscription", b =>
|
| 340 |
+
{
|
| 341 |
+
b.Property<Guid>("Id")
|
| 342 |
+
.ValueGeneratedOnAdd()
|
| 343 |
+
.HasColumnType("uniqueidentifier")
|
| 344 |
+
.HasDefaultValueSql("(newid())");
|
| 345 |
+
|
| 346 |
+
b.Property<DateTime>("ExpiryDate")
|
| 347 |
+
.HasColumnType("datetime");
|
| 348 |
+
|
| 349 |
+
b.Property<string>("ExternalRedemptionId")
|
| 350 |
+
.HasMaxLength(100)
|
| 351 |
+
.HasColumnType("nvarchar(100)");
|
| 352 |
+
|
| 353 |
+
b.Property<bool>("IsActive")
|
| 354 |
+
.ValueGeneratedOnAdd()
|
| 355 |
+
.HasColumnType("bit")
|
| 356 |
+
.HasDefaultValue(true);
|
| 357 |
+
|
| 358 |
+
b.Property<int>("MembershipId")
|
| 359 |
+
.HasColumnType("int");
|
| 360 |
+
|
| 361 |
+
b.Property<DateTime>("StartDate")
|
| 362 |
+
.HasColumnType("datetime");
|
| 363 |
+
|
| 364 |
+
b.Property<string>("Status")
|
| 365 |
+
.IsRequired()
|
| 366 |
+
.ValueGeneratedOnAdd()
|
| 367 |
+
.HasMaxLength(20)
|
| 368 |
+
.HasColumnType("nvarchar(20)")
|
| 369 |
+
.HasDefaultValue("Active");
|
| 370 |
+
|
| 371 |
+
b.Property<Guid>("UserId")
|
| 372 |
+
.HasColumnType("uniqueidentifier");
|
| 373 |
+
|
| 374 |
+
b.HasKey("Id");
|
| 375 |
+
|
| 376 |
+
b.HasIndex("MembershipId");
|
| 377 |
+
|
| 378 |
+
b.HasIndex("UserId");
|
| 379 |
+
|
| 380 |
+
b.ToTable("UserSubscriptions");
|
| 381 |
+
});
|
| 382 |
+
|
| 383 |
+
modelBuilder.Entity("BookCategory", b =>
|
| 384 |
+
{
|
| 385 |
+
b.HasOne("DbConnect.Entities.Book", null)
|
| 386 |
+
.WithMany()
|
| 387 |
+
.HasForeignKey("BookId")
|
| 388 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 389 |
+
.IsRequired()
|
| 390 |
+
.HasConstraintName("FK_BookCategories_Books");
|
| 391 |
+
|
| 392 |
+
b.HasOne("DbConnect.Entities.Category", null)
|
| 393 |
+
.WithMany()
|
| 394 |
+
.HasForeignKey("CategoryId")
|
| 395 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 396 |
+
.IsRequired()
|
| 397 |
+
.HasConstraintName("FK_BookCategories_Categories");
|
| 398 |
+
});
|
| 399 |
+
|
| 400 |
+
modelBuilder.Entity("DbConnect.Entities.Borrowing", b =>
|
| 401 |
+
{
|
| 402 |
+
b.HasOne("DbConnect.Entities.Book", "Book")
|
| 403 |
+
.WithMany("Borrowings")
|
| 404 |
+
.HasForeignKey("BookId")
|
| 405 |
+
.IsRequired()
|
| 406 |
+
.HasConstraintName("FK_Borrowings_Books");
|
| 407 |
+
|
| 408 |
+
b.HasOne("DbConnect.Entities.User", "User")
|
| 409 |
+
.WithMany("Borrowings")
|
| 410 |
+
.HasForeignKey("UserId")
|
| 411 |
+
.IsRequired()
|
| 412 |
+
.HasConstraintName("FK_Borrowings_Users");
|
| 413 |
+
|
| 414 |
+
b.Navigation("Book");
|
| 415 |
+
|
| 416 |
+
b.Navigation("User");
|
| 417 |
+
});
|
| 418 |
+
|
| 419 |
+
modelBuilder.Entity("DbConnect.Entities.Notification", b =>
|
| 420 |
+
{
|
| 421 |
+
b.HasOne("DbConnect.Entities.User", "User")
|
| 422 |
+
.WithMany()
|
| 423 |
+
.HasForeignKey("UserId")
|
| 424 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 425 |
+
.IsRequired()
|
| 426 |
+
.HasConstraintName("FK_Notifications_Users");
|
| 427 |
+
|
| 428 |
+
b.Navigation("User");
|
| 429 |
+
});
|
| 430 |
+
|
| 431 |
+
modelBuilder.Entity("DbConnect.Entities.UserSubscription", b =>
|
| 432 |
+
{
|
| 433 |
+
b.HasOne("DbConnect.Entities.Membership", "Membership")
|
| 434 |
+
.WithMany("UserSubscriptions")
|
| 435 |
+
.HasForeignKey("MembershipId")
|
| 436 |
+
.IsRequired()
|
| 437 |
+
.HasConstraintName("FK_UserSubscriptions_Memberships");
|
| 438 |
+
|
| 439 |
+
b.HasOne("DbConnect.Entities.User", "User")
|
| 440 |
+
.WithMany("UserSubscriptions")
|
| 441 |
+
.HasForeignKey("UserId")
|
| 442 |
+
.IsRequired()
|
| 443 |
+
.HasConstraintName("FK_UserSubscriptions_Users");
|
| 444 |
+
|
| 445 |
+
b.Navigation("Membership");
|
| 446 |
+
|
| 447 |
+
b.Navigation("User");
|
| 448 |
+
});
|
| 449 |
+
|
| 450 |
+
modelBuilder.Entity("DbConnect.Entities.Book", b =>
|
| 451 |
+
{
|
| 452 |
+
b.Navigation("Borrowings");
|
| 453 |
+
});
|
| 454 |
+
|
| 455 |
+
modelBuilder.Entity("DbConnect.Entities.Membership", b =>
|
| 456 |
+
{
|
| 457 |
+
b.Navigation("UserSubscriptions");
|
| 458 |
+
});
|
| 459 |
+
|
| 460 |
+
modelBuilder.Entity("DbConnect.Entities.User", b =>
|
| 461 |
+
{
|
| 462 |
+
b.Navigation("Borrowings");
|
| 463 |
+
|
| 464 |
+
b.Navigation("UserSubscriptions");
|
| 465 |
+
});
|
| 466 |
+
#pragma warning restore 612, 618
|
| 467 |
+
}
|
| 468 |
+
}
|
| 469 |
+
}
|
DbConnect/Migrations/20260427043736_AddNotifications.cs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using System;
|
| 2 |
+
using Microsoft.EntityFrameworkCore.Migrations;
|
| 3 |
+
|
| 4 |
+
#nullable disable
|
| 5 |
+
|
| 6 |
+
namespace DbConnect.Migrations
|
| 7 |
+
{
|
| 8 |
+
/// <inheritdoc />
|
| 9 |
+
public partial class AddNotifications : Migration
|
| 10 |
+
{
|
| 11 |
+
/// <inheritdoc />
|
| 12 |
+
protected override void Up(MigrationBuilder migrationBuilder)
|
| 13 |
+
{
|
| 14 |
+
migrationBuilder.CreateTable(
|
| 15 |
+
name: "Notifications",
|
| 16 |
+
columns: table => new
|
| 17 |
+
{
|
| 18 |
+
Id = table.Column<int>(type: "int", nullable: false)
|
| 19 |
+
.Annotation("SqlServer:Identity", "1, 1"),
|
| 20 |
+
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
| 21 |
+
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
| 22 |
+
Message = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
|
| 23 |
+
Type = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false, defaultValue: "Info"),
|
| 24 |
+
ActionLink = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
| 25 |
+
ActionText = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
| 26 |
+
IsRead = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
| 27 |
+
CreatedAt = table.Column<DateTime>(type: "datetime", nullable: false, defaultValueSql: "(getdate())")
|
| 28 |
+
},
|
| 29 |
+
constraints: table =>
|
| 30 |
+
{
|
| 31 |
+
table.PrimaryKey("PK_Notifications", x => x.Id);
|
| 32 |
+
table.ForeignKey(
|
| 33 |
+
name: "FK_Notifications_Users",
|
| 34 |
+
column: x => x.UserId,
|
| 35 |
+
principalTable: "Users",
|
| 36 |
+
principalColumn: "Id",
|
| 37 |
+
onDelete: ReferentialAction.Cascade);
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
migrationBuilder.CreateIndex(
|
| 41 |
+
name: "IX_Notifications_UserId",
|
| 42 |
+
table: "Notifications",
|
| 43 |
+
column: "UserId");
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/// <inheritdoc />
|
| 47 |
+
protected override void Down(MigrationBuilder migrationBuilder)
|
| 48 |
+
{
|
| 49 |
+
migrationBuilder.DropTable(
|
| 50 |
+
name: "BookCategories");
|
| 51 |
+
|
| 52 |
+
migrationBuilder.DropTable(
|
| 53 |
+
name: "Borrowings");
|
| 54 |
+
|
| 55 |
+
migrationBuilder.DropTable(
|
| 56 |
+
name: "Notifications");
|
| 57 |
+
|
| 58 |
+
migrationBuilder.DropTable(
|
| 59 |
+
name: "UserSubscriptions");
|
| 60 |
+
|
| 61 |
+
migrationBuilder.DropTable(
|
| 62 |
+
name: "Categories");
|
| 63 |
+
|
| 64 |
+
migrationBuilder.DropTable(
|
| 65 |
+
name: "Books");
|
| 66 |
+
|
| 67 |
+
migrationBuilder.DropTable(
|
| 68 |
+
name: "Memberships");
|
| 69 |
+
|
| 70 |
+
migrationBuilder.DropTable(
|
| 71 |
+
name: "Users");
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
}
|
DbConnect/Migrations/20260427052139_UpdateUserWithFcmToken.Designer.cs
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// <auto-generated />
|
| 2 |
+
using System;
|
| 3 |
+
using DbConnect.Data;
|
| 4 |
+
using Microsoft.EntityFrameworkCore;
|
| 5 |
+
using Microsoft.EntityFrameworkCore.Infrastructure;
|
| 6 |
+
using Microsoft.EntityFrameworkCore.Metadata;
|
| 7 |
+
using Microsoft.EntityFrameworkCore.Migrations;
|
| 8 |
+
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
| 9 |
+
|
| 10 |
+
#nullable disable
|
| 11 |
+
|
| 12 |
+
namespace DbConnect.Migrations
|
| 13 |
+
{
|
| 14 |
+
[DbContext(typeof(AppDbContext))]
|
| 15 |
+
[Migration("20260427052139_UpdateUserWithFcmToken")]
|
| 16 |
+
partial class UpdateUserWithFcmToken
|
| 17 |
+
{
|
| 18 |
+
/// <inheritdoc />
|
| 19 |
+
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
| 20 |
+
{
|
| 21 |
+
#pragma warning disable 612, 618
|
| 22 |
+
modelBuilder
|
| 23 |
+
.HasAnnotation("ProductVersion", "9.0.0")
|
| 24 |
+
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
| 25 |
+
|
| 26 |
+
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
| 27 |
+
|
| 28 |
+
modelBuilder.Entity("BookCategory", b =>
|
| 29 |
+
{
|
| 30 |
+
b.Property<int>("BookId")
|
| 31 |
+
.HasColumnType("int");
|
| 32 |
+
|
| 33 |
+
b.Property<int>("CategoryId")
|
| 34 |
+
.HasColumnType("int");
|
| 35 |
+
|
| 36 |
+
b.HasKey("BookId", "CategoryId")
|
| 37 |
+
.HasName("PK__BookCate__9C7051A74E7D221D");
|
| 38 |
+
|
| 39 |
+
b.HasIndex("CategoryId");
|
| 40 |
+
|
| 41 |
+
b.ToTable("BookCategories", (string)null);
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
modelBuilder.Entity("DbConnect.Entities.Book", b =>
|
| 45 |
+
{
|
| 46 |
+
b.Property<int>("Id")
|
| 47 |
+
.ValueGeneratedOnAdd()
|
| 48 |
+
.HasColumnType("int");
|
| 49 |
+
|
| 50 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 51 |
+
|
| 52 |
+
b.Property<string>("Author")
|
| 53 |
+
.IsRequired()
|
| 54 |
+
.HasMaxLength(100)
|
| 55 |
+
.HasColumnType("nvarchar(100)");
|
| 56 |
+
|
| 57 |
+
b.Property<int>("AvailableCopies")
|
| 58 |
+
.ValueGeneratedOnAdd()
|
| 59 |
+
.HasColumnType("int")
|
| 60 |
+
.HasDefaultValue(1);
|
| 61 |
+
|
| 62 |
+
b.Property<string>("CoverUrl")
|
| 63 |
+
.HasColumnType("nvarchar(max)");
|
| 64 |
+
|
| 65 |
+
b.Property<DateTime>("CreatedAt")
|
| 66 |
+
.ValueGeneratedOnAdd()
|
| 67 |
+
.HasColumnType("datetime")
|
| 68 |
+
.HasDefaultValueSql("(getdate())");
|
| 69 |
+
|
| 70 |
+
b.Property<string>("Description")
|
| 71 |
+
.HasColumnType("nvarchar(max)");
|
| 72 |
+
|
| 73 |
+
b.Property<bool>("IsActive")
|
| 74 |
+
.ValueGeneratedOnAdd()
|
| 75 |
+
.HasColumnType("bit")
|
| 76 |
+
.HasDefaultValue(true);
|
| 77 |
+
|
| 78 |
+
b.Property<string>("Isbn")
|
| 79 |
+
.IsRequired()
|
| 80 |
+
.HasMaxLength(20)
|
| 81 |
+
.HasColumnType("nvarchar(20)")
|
| 82 |
+
.HasColumnName("ISBN");
|
| 83 |
+
|
| 84 |
+
b.Property<string>("Status")
|
| 85 |
+
.IsRequired()
|
| 86 |
+
.ValueGeneratedOnAdd()
|
| 87 |
+
.HasMaxLength(20)
|
| 88 |
+
.HasColumnType("nvarchar(20)")
|
| 89 |
+
.HasDefaultValue("Available");
|
| 90 |
+
|
| 91 |
+
b.Property<string>("Title")
|
| 92 |
+
.IsRequired()
|
| 93 |
+
.HasMaxLength(200)
|
| 94 |
+
.HasColumnType("nvarchar(200)");
|
| 95 |
+
|
| 96 |
+
b.Property<int>("TotalCopies")
|
| 97 |
+
.ValueGeneratedOnAdd()
|
| 98 |
+
.HasColumnType("int")
|
| 99 |
+
.HasDefaultValue(1);
|
| 100 |
+
|
| 101 |
+
b.Property<DateTime?>("UpdatedAt")
|
| 102 |
+
.HasColumnType("datetime");
|
| 103 |
+
|
| 104 |
+
b.HasKey("Id")
|
| 105 |
+
.HasName("PK__Books__3214EC07AE1974C8");
|
| 106 |
+
|
| 107 |
+
b.HasIndex(new[] { "Isbn" }, "UQ__Books__447D36EA79CF66CE")
|
| 108 |
+
.IsUnique();
|
| 109 |
+
|
| 110 |
+
b.ToTable("Books");
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
modelBuilder.Entity("DbConnect.Entities.Borrowing", b =>
|
| 114 |
+
{
|
| 115 |
+
b.Property<Guid>("Id")
|
| 116 |
+
.ValueGeneratedOnAdd()
|
| 117 |
+
.HasColumnType("uniqueidentifier")
|
| 118 |
+
.HasDefaultValueSql("(newid())");
|
| 119 |
+
|
| 120 |
+
b.Property<int>("BookId")
|
| 121 |
+
.HasColumnType("int");
|
| 122 |
+
|
| 123 |
+
b.Property<DateTime>("BorrowDate")
|
| 124 |
+
.ValueGeneratedOnAdd()
|
| 125 |
+
.HasColumnType("datetime")
|
| 126 |
+
.HasDefaultValueSql("(getdate())");
|
| 127 |
+
|
| 128 |
+
b.Property<DateTime>("DueDate")
|
| 129 |
+
.HasColumnType("datetime");
|
| 130 |
+
|
| 131 |
+
b.Property<decimal>("FineAmount")
|
| 132 |
+
.HasColumnType("decimal(10, 2)");
|
| 133 |
+
|
| 134 |
+
b.Property<bool>("IsFinePaid")
|
| 135 |
+
.HasColumnType("bit");
|
| 136 |
+
|
| 137 |
+
b.Property<DateTime?>("ReturnDate")
|
| 138 |
+
.HasColumnType("datetime");
|
| 139 |
+
|
| 140 |
+
b.Property<string>("Status")
|
| 141 |
+
.IsRequired()
|
| 142 |
+
.ValueGeneratedOnAdd()
|
| 143 |
+
.HasMaxLength(20)
|
| 144 |
+
.HasColumnType("nvarchar(20)")
|
| 145 |
+
.HasDefaultValue("Active");
|
| 146 |
+
|
| 147 |
+
b.Property<Guid>("UserId")
|
| 148 |
+
.HasColumnType("uniqueidentifier");
|
| 149 |
+
|
| 150 |
+
b.HasKey("Id");
|
| 151 |
+
|
| 152 |
+
b.HasIndex("BookId");
|
| 153 |
+
|
| 154 |
+
b.HasIndex("UserId");
|
| 155 |
+
|
| 156 |
+
b.ToTable("Borrowings");
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
+
modelBuilder.Entity("DbConnect.Entities.Category", b =>
|
| 160 |
+
{
|
| 161 |
+
b.Property<int>("Id")
|
| 162 |
+
.ValueGeneratedOnAdd()
|
| 163 |
+
.HasColumnType("int");
|
| 164 |
+
|
| 165 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 166 |
+
|
| 167 |
+
b.Property<string>("Name")
|
| 168 |
+
.IsRequired()
|
| 169 |
+
.HasMaxLength(100)
|
| 170 |
+
.HasColumnType("nvarchar(100)");
|
| 171 |
+
|
| 172 |
+
b.HasKey("Id")
|
| 173 |
+
.HasName("PK__Categori__3214EC0751B5797D");
|
| 174 |
+
|
| 175 |
+
b.HasIndex(new[] { "Name" }, "UQ__Categori__737584F622B16D3A")
|
| 176 |
+
.IsUnique();
|
| 177 |
+
|
| 178 |
+
b.ToTable("Categories");
|
| 179 |
+
});
|
| 180 |
+
|
| 181 |
+
modelBuilder.Entity("DbConnect.Entities.Membership", b =>
|
| 182 |
+
{
|
| 183 |
+
b.Property<int>("Id")
|
| 184 |
+
.ValueGeneratedOnAdd()
|
| 185 |
+
.HasColumnType("int");
|
| 186 |
+
|
| 187 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 188 |
+
|
| 189 |
+
b.Property<int>("BorrowingDays")
|
| 190 |
+
.HasColumnType("int");
|
| 191 |
+
|
| 192 |
+
b.Property<int>("DurationMonths")
|
| 193 |
+
.HasColumnType("int");
|
| 194 |
+
|
| 195 |
+
b.Property<int>("MaxBooks")
|
| 196 |
+
.HasColumnType("int");
|
| 197 |
+
|
| 198 |
+
b.Property<decimal>("Price")
|
| 199 |
+
.HasColumnType("decimal(10, 2)");
|
| 200 |
+
|
| 201 |
+
b.Property<string>("RewardId")
|
| 202 |
+
.HasMaxLength(100)
|
| 203 |
+
.HasColumnType("nvarchar(100)");
|
| 204 |
+
|
| 205 |
+
b.Property<string>("Type")
|
| 206 |
+
.IsRequired()
|
| 207 |
+
.HasMaxLength(50)
|
| 208 |
+
.HasColumnType("nvarchar(50)");
|
| 209 |
+
|
| 210 |
+
b.HasKey("Id")
|
| 211 |
+
.HasName("PK__Membersh__3214EC07A6F5A55F");
|
| 212 |
+
|
| 213 |
+
b.HasIndex(new[] { "Type" }, "IX_Memberships_Type")
|
| 214 |
+
.IsUnique();
|
| 215 |
+
|
| 216 |
+
b.ToTable("Memberships");
|
| 217 |
+
});
|
| 218 |
+
|
| 219 |
+
modelBuilder.Entity("DbConnect.Entities.Notification", b =>
|
| 220 |
+
{
|
| 221 |
+
b.Property<int>("Id")
|
| 222 |
+
.ValueGeneratedOnAdd()
|
| 223 |
+
.HasColumnType("int");
|
| 224 |
+
|
| 225 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 226 |
+
|
| 227 |
+
b.Property<string>("ActionLink")
|
| 228 |
+
.HasMaxLength(500)
|
| 229 |
+
.HasColumnType("nvarchar(500)");
|
| 230 |
+
|
| 231 |
+
b.Property<string>("ActionText")
|
| 232 |
+
.HasMaxLength(100)
|
| 233 |
+
.HasColumnType("nvarchar(100)");
|
| 234 |
+
|
| 235 |
+
b.Property<DateTime>("CreatedAt")
|
| 236 |
+
.ValueGeneratedOnAdd()
|
| 237 |
+
.HasColumnType("datetime")
|
| 238 |
+
.HasDefaultValueSql("(getdate())");
|
| 239 |
+
|
| 240 |
+
b.Property<bool>("IsRead")
|
| 241 |
+
.ValueGeneratedOnAdd()
|
| 242 |
+
.HasColumnType("bit")
|
| 243 |
+
.HasDefaultValue(false);
|
| 244 |
+
|
| 245 |
+
b.Property<string>("Message")
|
| 246 |
+
.IsRequired()
|
| 247 |
+
.HasMaxLength(1000)
|
| 248 |
+
.HasColumnType("nvarchar(1000)");
|
| 249 |
+
|
| 250 |
+
b.Property<string>("Title")
|
| 251 |
+
.IsRequired()
|
| 252 |
+
.HasMaxLength(200)
|
| 253 |
+
.HasColumnType("nvarchar(200)");
|
| 254 |
+
|
| 255 |
+
b.Property<string>("Type")
|
| 256 |
+
.IsRequired()
|
| 257 |
+
.ValueGeneratedOnAdd()
|
| 258 |
+
.HasMaxLength(50)
|
| 259 |
+
.HasColumnType("nvarchar(50)")
|
| 260 |
+
.HasDefaultValue("Info");
|
| 261 |
+
|
| 262 |
+
b.Property<Guid>("UserId")
|
| 263 |
+
.HasColumnType("uniqueidentifier");
|
| 264 |
+
|
| 265 |
+
b.HasKey("Id");
|
| 266 |
+
|
| 267 |
+
b.HasIndex("UserId");
|
| 268 |
+
|
| 269 |
+
b.ToTable("Notifications");
|
| 270 |
+
});
|
| 271 |
+
|
| 272 |
+
modelBuilder.Entity("DbConnect.Entities.User", b =>
|
| 273 |
+
{
|
| 274 |
+
b.Property<Guid>("Id")
|
| 275 |
+
.ValueGeneratedOnAdd()
|
| 276 |
+
.HasColumnType("uniqueidentifier")
|
| 277 |
+
.HasDefaultValueSql("(newid())");
|
| 278 |
+
|
| 279 |
+
b.Property<string>("Address")
|
| 280 |
+
.HasMaxLength(255)
|
| 281 |
+
.HasColumnType("nvarchar(255)");
|
| 282 |
+
|
| 283 |
+
b.Property<bool?>("BanStatus")
|
| 284 |
+
.HasColumnType("bit");
|
| 285 |
+
|
| 286 |
+
b.Property<DateTime>("CreatedAt")
|
| 287 |
+
.ValueGeneratedOnAdd()
|
| 288 |
+
.HasColumnType("datetime")
|
| 289 |
+
.HasDefaultValueSql("(getdate())");
|
| 290 |
+
|
| 291 |
+
b.Property<string>("Email")
|
| 292 |
+
.IsRequired()
|
| 293 |
+
.HasMaxLength(100)
|
| 294 |
+
.HasColumnType("nvarchar(100)");
|
| 295 |
+
|
| 296 |
+
b.Property<string>("FcmToken")
|
| 297 |
+
.HasMaxLength(500)
|
| 298 |
+
.HasColumnType("nvarchar(500)");
|
| 299 |
+
|
| 300 |
+
b.Property<string>("FullName")
|
| 301 |
+
.IsRequired()
|
| 302 |
+
.HasMaxLength(100)
|
| 303 |
+
.HasColumnType("nvarchar(100)");
|
| 304 |
+
|
| 305 |
+
b.Property<bool>("IsActive")
|
| 306 |
+
.ValueGeneratedOnAdd()
|
| 307 |
+
.HasColumnType("bit")
|
| 308 |
+
.HasDefaultValue(true);
|
| 309 |
+
|
| 310 |
+
b.Property<string>("PasswordHash")
|
| 311 |
+
.IsRequired()
|
| 312 |
+
.HasColumnType("nvarchar(max)");
|
| 313 |
+
|
| 314 |
+
b.Property<string>("PhoneNumber")
|
| 315 |
+
.HasMaxLength(20)
|
| 316 |
+
.HasColumnType("nvarchar(20)");
|
| 317 |
+
|
| 318 |
+
b.Property<string>("Role")
|
| 319 |
+
.IsRequired()
|
| 320 |
+
.ValueGeneratedOnAdd()
|
| 321 |
+
.HasMaxLength(20)
|
| 322 |
+
.HasColumnType("nvarchar(20)")
|
| 323 |
+
.HasDefaultValue("User");
|
| 324 |
+
|
| 325 |
+
b.Property<string>("StudentId")
|
| 326 |
+
.HasMaxLength(20)
|
| 327 |
+
.HasColumnType("nvarchar(20)");
|
| 328 |
+
|
| 329 |
+
b.Property<DateTime?>("SuspensionEndDate")
|
| 330 |
+
.HasColumnType("datetime");
|
| 331 |
+
|
| 332 |
+
b.Property<DateTime?>("UpdatedAt")
|
| 333 |
+
.HasColumnType("datetime");
|
| 334 |
+
|
| 335 |
+
b.HasKey("Id");
|
| 336 |
+
|
| 337 |
+
b.HasIndex(new[] { "Email" }, "UQ_Users_Email")
|
| 338 |
+
.IsUnique();
|
| 339 |
+
|
| 340 |
+
b.ToTable("Users");
|
| 341 |
+
});
|
| 342 |
+
|
| 343 |
+
modelBuilder.Entity("DbConnect.Entities.UserSubscription", b =>
|
| 344 |
+
{
|
| 345 |
+
b.Property<Guid>("Id")
|
| 346 |
+
.ValueGeneratedOnAdd()
|
| 347 |
+
.HasColumnType("uniqueidentifier")
|
| 348 |
+
.HasDefaultValueSql("(newid())");
|
| 349 |
+
|
| 350 |
+
b.Property<DateTime>("ExpiryDate")
|
| 351 |
+
.HasColumnType("datetime");
|
| 352 |
+
|
| 353 |
+
b.Property<string>("ExternalRedemptionId")
|
| 354 |
+
.HasMaxLength(100)
|
| 355 |
+
.HasColumnType("nvarchar(100)");
|
| 356 |
+
|
| 357 |
+
b.Property<bool>("IsActive")
|
| 358 |
+
.ValueGeneratedOnAdd()
|
| 359 |
+
.HasColumnType("bit")
|
| 360 |
+
.HasDefaultValue(true);
|
| 361 |
+
|
| 362 |
+
b.Property<int>("MembershipId")
|
| 363 |
+
.HasColumnType("int");
|
| 364 |
+
|
| 365 |
+
b.Property<DateTime>("StartDate")
|
| 366 |
+
.HasColumnType("datetime");
|
| 367 |
+
|
| 368 |
+
b.Property<string>("Status")
|
| 369 |
+
.IsRequired()
|
| 370 |
+
.ValueGeneratedOnAdd()
|
| 371 |
+
.HasMaxLength(20)
|
| 372 |
+
.HasColumnType("nvarchar(20)")
|
| 373 |
+
.HasDefaultValue("Active");
|
| 374 |
+
|
| 375 |
+
b.Property<Guid>("UserId")
|
| 376 |
+
.HasColumnType("uniqueidentifier");
|
| 377 |
+
|
| 378 |
+
b.HasKey("Id");
|
| 379 |
+
|
| 380 |
+
b.HasIndex("MembershipId");
|
| 381 |
+
|
| 382 |
+
b.HasIndex("UserId");
|
| 383 |
+
|
| 384 |
+
b.ToTable("UserSubscriptions");
|
| 385 |
+
});
|
| 386 |
+
|
| 387 |
+
modelBuilder.Entity("BookCategory", b =>
|
| 388 |
+
{
|
| 389 |
+
b.HasOne("DbConnect.Entities.Book", null)
|
| 390 |
+
.WithMany()
|
| 391 |
+
.HasForeignKey("BookId")
|
| 392 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 393 |
+
.IsRequired()
|
| 394 |
+
.HasConstraintName("FK_BookCategories_Books");
|
| 395 |
+
|
| 396 |
+
b.HasOne("DbConnect.Entities.Category", null)
|
| 397 |
+
.WithMany()
|
| 398 |
+
.HasForeignKey("CategoryId")
|
| 399 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 400 |
+
.IsRequired()
|
| 401 |
+
.HasConstraintName("FK_BookCategories_Categories");
|
| 402 |
+
});
|
| 403 |
+
|
| 404 |
+
modelBuilder.Entity("DbConnect.Entities.Borrowing", b =>
|
| 405 |
+
{
|
| 406 |
+
b.HasOne("DbConnect.Entities.Book", "Book")
|
| 407 |
+
.WithMany("Borrowings")
|
| 408 |
+
.HasForeignKey("BookId")
|
| 409 |
+
.IsRequired()
|
| 410 |
+
.HasConstraintName("FK_Borrowings_Books");
|
| 411 |
+
|
| 412 |
+
b.HasOne("DbConnect.Entities.User", "User")
|
| 413 |
+
.WithMany("Borrowings")
|
| 414 |
+
.HasForeignKey("UserId")
|
| 415 |
+
.IsRequired()
|
| 416 |
+
.HasConstraintName("FK_Borrowings_Users");
|
| 417 |
+
|
| 418 |
+
b.Navigation("Book");
|
| 419 |
+
|
| 420 |
+
b.Navigation("User");
|
| 421 |
+
});
|
| 422 |
+
|
| 423 |
+
modelBuilder.Entity("DbConnect.Entities.Notification", b =>
|
| 424 |
+
{
|
| 425 |
+
b.HasOne("DbConnect.Entities.User", "User")
|
| 426 |
+
.WithMany()
|
| 427 |
+
.HasForeignKey("UserId")
|
| 428 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 429 |
+
.IsRequired()
|
| 430 |
+
.HasConstraintName("FK_Notifications_Users");
|
| 431 |
+
|
| 432 |
+
b.Navigation("User");
|
| 433 |
+
});
|
| 434 |
+
|
| 435 |
+
modelBuilder.Entity("DbConnect.Entities.UserSubscription", b =>
|
| 436 |
+
{
|
| 437 |
+
b.HasOne("DbConnect.Entities.Membership", "Membership")
|
| 438 |
+
.WithMany("UserSubscriptions")
|
| 439 |
+
.HasForeignKey("MembershipId")
|
| 440 |
+
.IsRequired()
|
| 441 |
+
.HasConstraintName("FK_UserSubscriptions_Memberships");
|
| 442 |
+
|
| 443 |
+
b.HasOne("DbConnect.Entities.User", "User")
|
| 444 |
+
.WithMany("UserSubscriptions")
|
| 445 |
+
.HasForeignKey("UserId")
|
| 446 |
+
.IsRequired()
|
| 447 |
+
.HasConstraintName("FK_UserSubscriptions_Users");
|
| 448 |
+
|
| 449 |
+
b.Navigation("Membership");
|
| 450 |
+
|
| 451 |
+
b.Navigation("User");
|
| 452 |
+
});
|
| 453 |
+
|
| 454 |
+
modelBuilder.Entity("DbConnect.Entities.Book", b =>
|
| 455 |
+
{
|
| 456 |
+
b.Navigation("Borrowings");
|
| 457 |
+
});
|
| 458 |
+
|
| 459 |
+
modelBuilder.Entity("DbConnect.Entities.Membership", b =>
|
| 460 |
+
{
|
| 461 |
+
b.Navigation("UserSubscriptions");
|
| 462 |
+
});
|
| 463 |
+
|
| 464 |
+
modelBuilder.Entity("DbConnect.Entities.User", b =>
|
| 465 |
+
{
|
| 466 |
+
b.Navigation("Borrowings");
|
| 467 |
+
|
| 468 |
+
b.Navigation("UserSubscriptions");
|
| 469 |
+
});
|
| 470 |
+
#pragma warning restore 612, 618
|
| 471 |
+
}
|
| 472 |
+
}
|
| 473 |
+
}
|
DbConnect/Migrations/20260427052139_UpdateUserWithFcmToken.cs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using Microsoft.EntityFrameworkCore.Migrations;
|
| 2 |
+
|
| 3 |
+
#nullable disable
|
| 4 |
+
|
| 5 |
+
namespace DbConnect.Migrations
|
| 6 |
+
{
|
| 7 |
+
/// <inheritdoc />
|
| 8 |
+
public partial class UpdateUserWithFcmToken : Migration
|
| 9 |
+
{
|
| 10 |
+
/// <inheritdoc />
|
| 11 |
+
protected override void Up(MigrationBuilder migrationBuilder)
|
| 12 |
+
{
|
| 13 |
+
migrationBuilder.AddColumn<string>(
|
| 14 |
+
name: "FcmToken",
|
| 15 |
+
table: "Users",
|
| 16 |
+
type: "nvarchar(500)",
|
| 17 |
+
maxLength: 500,
|
| 18 |
+
nullable: true);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
/// <inheritdoc />
|
| 22 |
+
protected override void Down(MigrationBuilder migrationBuilder)
|
| 23 |
+
{
|
| 24 |
+
migrationBuilder.DropColumn(
|
| 25 |
+
name: "FcmToken",
|
| 26 |
+
table: "Users");
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
}
|
DbConnect/Migrations/20260427071940_AddWishlistItems.Designer.cs
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// <auto-generated />
|
| 2 |
+
using System;
|
| 3 |
+
using DbConnect.Data;
|
| 4 |
+
using Microsoft.EntityFrameworkCore;
|
| 5 |
+
using Microsoft.EntityFrameworkCore.Infrastructure;
|
| 6 |
+
using Microsoft.EntityFrameworkCore.Metadata;
|
| 7 |
+
using Microsoft.EntityFrameworkCore.Migrations;
|
| 8 |
+
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
| 9 |
+
|
| 10 |
+
#nullable disable
|
| 11 |
+
|
| 12 |
+
namespace DbConnect.Migrations
|
| 13 |
+
{
|
| 14 |
+
[DbContext(typeof(AppDbContext))]
|
| 15 |
+
[Migration("20260427071940_AddWishlistItems")]
|
| 16 |
+
partial class AddWishlistItems
|
| 17 |
+
{
|
| 18 |
+
/// <inheritdoc />
|
| 19 |
+
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
| 20 |
+
{
|
| 21 |
+
#pragma warning disable 612, 618
|
| 22 |
+
modelBuilder
|
| 23 |
+
.HasAnnotation("ProductVersion", "9.0.0")
|
| 24 |
+
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
| 25 |
+
|
| 26 |
+
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
| 27 |
+
|
| 28 |
+
modelBuilder.Entity("BookCategory", b =>
|
| 29 |
+
{
|
| 30 |
+
b.Property<int>("BookId")
|
| 31 |
+
.HasColumnType("int");
|
| 32 |
+
|
| 33 |
+
b.Property<int>("CategoryId")
|
| 34 |
+
.HasColumnType("int");
|
| 35 |
+
|
| 36 |
+
b.HasKey("BookId", "CategoryId")
|
| 37 |
+
.HasName("PK__BookCate__9C7051A74E7D221D");
|
| 38 |
+
|
| 39 |
+
b.HasIndex("CategoryId");
|
| 40 |
+
|
| 41 |
+
b.ToTable("BookCategories", (string)null);
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
modelBuilder.Entity("DbConnect.Entities.Book", b =>
|
| 45 |
+
{
|
| 46 |
+
b.Property<int>("Id")
|
| 47 |
+
.ValueGeneratedOnAdd()
|
| 48 |
+
.HasColumnType("int");
|
| 49 |
+
|
| 50 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 51 |
+
|
| 52 |
+
b.Property<string>("Author")
|
| 53 |
+
.IsRequired()
|
| 54 |
+
.HasMaxLength(100)
|
| 55 |
+
.HasColumnType("nvarchar(100)");
|
| 56 |
+
|
| 57 |
+
b.Property<int>("AvailableCopies")
|
| 58 |
+
.ValueGeneratedOnAdd()
|
| 59 |
+
.HasColumnType("int")
|
| 60 |
+
.HasDefaultValue(1);
|
| 61 |
+
|
| 62 |
+
b.Property<string>("CoverUrl")
|
| 63 |
+
.HasColumnType("nvarchar(max)");
|
| 64 |
+
|
| 65 |
+
b.Property<DateTime>("CreatedAt")
|
| 66 |
+
.ValueGeneratedOnAdd()
|
| 67 |
+
.HasColumnType("datetime")
|
| 68 |
+
.HasDefaultValueSql("(getdate())");
|
| 69 |
+
|
| 70 |
+
b.Property<string>("Description")
|
| 71 |
+
.HasColumnType("nvarchar(max)");
|
| 72 |
+
|
| 73 |
+
b.Property<bool>("IsActive")
|
| 74 |
+
.ValueGeneratedOnAdd()
|
| 75 |
+
.HasColumnType("bit")
|
| 76 |
+
.HasDefaultValue(true);
|
| 77 |
+
|
| 78 |
+
b.Property<string>("Isbn")
|
| 79 |
+
.IsRequired()
|
| 80 |
+
.HasMaxLength(20)
|
| 81 |
+
.HasColumnType("nvarchar(20)")
|
| 82 |
+
.HasColumnName("ISBN");
|
| 83 |
+
|
| 84 |
+
b.Property<string>("Status")
|
| 85 |
+
.IsRequired()
|
| 86 |
+
.ValueGeneratedOnAdd()
|
| 87 |
+
.HasMaxLength(20)
|
| 88 |
+
.HasColumnType("nvarchar(20)")
|
| 89 |
+
.HasDefaultValue("Available");
|
| 90 |
+
|
| 91 |
+
b.Property<string>("Title")
|
| 92 |
+
.IsRequired()
|
| 93 |
+
.HasMaxLength(200)
|
| 94 |
+
.HasColumnType("nvarchar(200)");
|
| 95 |
+
|
| 96 |
+
b.Property<int>("TotalCopies")
|
| 97 |
+
.ValueGeneratedOnAdd()
|
| 98 |
+
.HasColumnType("int")
|
| 99 |
+
.HasDefaultValue(1);
|
| 100 |
+
|
| 101 |
+
b.Property<DateTime?>("UpdatedAt")
|
| 102 |
+
.HasColumnType("datetime");
|
| 103 |
+
|
| 104 |
+
b.HasKey("Id")
|
| 105 |
+
.HasName("PK__Books__3214EC07AE1974C8");
|
| 106 |
+
|
| 107 |
+
b.HasIndex(new[] { "Isbn" }, "UQ__Books__447D36EA79CF66CE")
|
| 108 |
+
.IsUnique();
|
| 109 |
+
|
| 110 |
+
b.ToTable("Books");
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
modelBuilder.Entity("DbConnect.Entities.Borrowing", b =>
|
| 114 |
+
{
|
| 115 |
+
b.Property<Guid>("Id")
|
| 116 |
+
.ValueGeneratedOnAdd()
|
| 117 |
+
.HasColumnType("uniqueidentifier")
|
| 118 |
+
.HasDefaultValueSql("(newid())");
|
| 119 |
+
|
| 120 |
+
b.Property<int>("BookId")
|
| 121 |
+
.HasColumnType("int");
|
| 122 |
+
|
| 123 |
+
b.Property<DateTime>("BorrowDate")
|
| 124 |
+
.ValueGeneratedOnAdd()
|
| 125 |
+
.HasColumnType("datetime")
|
| 126 |
+
.HasDefaultValueSql("(getdate())");
|
| 127 |
+
|
| 128 |
+
b.Property<DateTime>("DueDate")
|
| 129 |
+
.HasColumnType("datetime");
|
| 130 |
+
|
| 131 |
+
b.Property<decimal>("FineAmount")
|
| 132 |
+
.HasColumnType("decimal(10, 2)");
|
| 133 |
+
|
| 134 |
+
b.Property<bool>("IsFinePaid")
|
| 135 |
+
.HasColumnType("bit");
|
| 136 |
+
|
| 137 |
+
b.Property<DateTime?>("ReturnDate")
|
| 138 |
+
.HasColumnType("datetime");
|
| 139 |
+
|
| 140 |
+
b.Property<string>("Status")
|
| 141 |
+
.IsRequired()
|
| 142 |
+
.ValueGeneratedOnAdd()
|
| 143 |
+
.HasMaxLength(20)
|
| 144 |
+
.HasColumnType("nvarchar(20)")
|
| 145 |
+
.HasDefaultValue("Active");
|
| 146 |
+
|
| 147 |
+
b.Property<Guid>("UserId")
|
| 148 |
+
.HasColumnType("uniqueidentifier");
|
| 149 |
+
|
| 150 |
+
b.HasKey("Id");
|
| 151 |
+
|
| 152 |
+
b.HasIndex("BookId");
|
| 153 |
+
|
| 154 |
+
b.HasIndex("UserId");
|
| 155 |
+
|
| 156 |
+
b.ToTable("Borrowings");
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
+
modelBuilder.Entity("DbConnect.Entities.Category", b =>
|
| 160 |
+
{
|
| 161 |
+
b.Property<int>("Id")
|
| 162 |
+
.ValueGeneratedOnAdd()
|
| 163 |
+
.HasColumnType("int");
|
| 164 |
+
|
| 165 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 166 |
+
|
| 167 |
+
b.Property<string>("Name")
|
| 168 |
+
.IsRequired()
|
| 169 |
+
.HasMaxLength(100)
|
| 170 |
+
.HasColumnType("nvarchar(100)");
|
| 171 |
+
|
| 172 |
+
b.HasKey("Id")
|
| 173 |
+
.HasName("PK__Categori__3214EC0751B5797D");
|
| 174 |
+
|
| 175 |
+
b.HasIndex(new[] { "Name" }, "UQ__Categori__737584F622B16D3A")
|
| 176 |
+
.IsUnique();
|
| 177 |
+
|
| 178 |
+
b.ToTable("Categories");
|
| 179 |
+
});
|
| 180 |
+
|
| 181 |
+
modelBuilder.Entity("DbConnect.Entities.Membership", b =>
|
| 182 |
+
{
|
| 183 |
+
b.Property<int>("Id")
|
| 184 |
+
.ValueGeneratedOnAdd()
|
| 185 |
+
.HasColumnType("int");
|
| 186 |
+
|
| 187 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 188 |
+
|
| 189 |
+
b.Property<int>("BorrowingDays")
|
| 190 |
+
.HasColumnType("int");
|
| 191 |
+
|
| 192 |
+
b.Property<int>("DurationMonths")
|
| 193 |
+
.HasColumnType("int");
|
| 194 |
+
|
| 195 |
+
b.Property<int>("MaxBooks")
|
| 196 |
+
.HasColumnType("int");
|
| 197 |
+
|
| 198 |
+
b.Property<decimal>("Price")
|
| 199 |
+
.HasColumnType("decimal(10, 2)");
|
| 200 |
+
|
| 201 |
+
b.Property<string>("RewardId")
|
| 202 |
+
.HasMaxLength(100)
|
| 203 |
+
.HasColumnType("nvarchar(100)");
|
| 204 |
+
|
| 205 |
+
b.Property<string>("Type")
|
| 206 |
+
.IsRequired()
|
| 207 |
+
.HasMaxLength(50)
|
| 208 |
+
.HasColumnType("nvarchar(50)");
|
| 209 |
+
|
| 210 |
+
b.HasKey("Id")
|
| 211 |
+
.HasName("PK__Membersh__3214EC07A6F5A55F");
|
| 212 |
+
|
| 213 |
+
b.HasIndex(new[] { "Type" }, "IX_Memberships_Type")
|
| 214 |
+
.IsUnique();
|
| 215 |
+
|
| 216 |
+
b.ToTable("Memberships");
|
| 217 |
+
});
|
| 218 |
+
|
| 219 |
+
modelBuilder.Entity("DbConnect.Entities.Notification", b =>
|
| 220 |
+
{
|
| 221 |
+
b.Property<int>("Id")
|
| 222 |
+
.ValueGeneratedOnAdd()
|
| 223 |
+
.HasColumnType("int");
|
| 224 |
+
|
| 225 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 226 |
+
|
| 227 |
+
b.Property<string>("ActionLink")
|
| 228 |
+
.HasMaxLength(500)
|
| 229 |
+
.HasColumnType("nvarchar(500)");
|
| 230 |
+
|
| 231 |
+
b.Property<string>("ActionText")
|
| 232 |
+
.HasMaxLength(100)
|
| 233 |
+
.HasColumnType("nvarchar(100)");
|
| 234 |
+
|
| 235 |
+
b.Property<DateTime>("CreatedAt")
|
| 236 |
+
.ValueGeneratedOnAdd()
|
| 237 |
+
.HasColumnType("datetime")
|
| 238 |
+
.HasDefaultValueSql("(getdate())");
|
| 239 |
+
|
| 240 |
+
b.Property<bool>("IsRead")
|
| 241 |
+
.ValueGeneratedOnAdd()
|
| 242 |
+
.HasColumnType("bit")
|
| 243 |
+
.HasDefaultValue(false);
|
| 244 |
+
|
| 245 |
+
b.Property<string>("Message")
|
| 246 |
+
.IsRequired()
|
| 247 |
+
.HasMaxLength(1000)
|
| 248 |
+
.HasColumnType("nvarchar(1000)");
|
| 249 |
+
|
| 250 |
+
b.Property<string>("Title")
|
| 251 |
+
.IsRequired()
|
| 252 |
+
.HasMaxLength(200)
|
| 253 |
+
.HasColumnType("nvarchar(200)");
|
| 254 |
+
|
| 255 |
+
b.Property<string>("Type")
|
| 256 |
+
.IsRequired()
|
| 257 |
+
.ValueGeneratedOnAdd()
|
| 258 |
+
.HasMaxLength(50)
|
| 259 |
+
.HasColumnType("nvarchar(50)")
|
| 260 |
+
.HasDefaultValue("Info");
|
| 261 |
+
|
| 262 |
+
b.Property<Guid>("UserId")
|
| 263 |
+
.HasColumnType("uniqueidentifier");
|
| 264 |
+
|
| 265 |
+
b.HasKey("Id");
|
| 266 |
+
|
| 267 |
+
b.HasIndex("UserId");
|
| 268 |
+
|
| 269 |
+
b.ToTable("Notifications");
|
| 270 |
+
});
|
| 271 |
+
|
| 272 |
+
modelBuilder.Entity("DbConnect.Entities.User", b =>
|
| 273 |
+
{
|
| 274 |
+
b.Property<Guid>("Id")
|
| 275 |
+
.ValueGeneratedOnAdd()
|
| 276 |
+
.HasColumnType("uniqueidentifier")
|
| 277 |
+
.HasDefaultValueSql("(newid())");
|
| 278 |
+
|
| 279 |
+
b.Property<string>("Address")
|
| 280 |
+
.HasMaxLength(255)
|
| 281 |
+
.HasColumnType("nvarchar(255)");
|
| 282 |
+
|
| 283 |
+
b.Property<bool?>("BanStatus")
|
| 284 |
+
.HasColumnType("bit");
|
| 285 |
+
|
| 286 |
+
b.Property<DateTime>("CreatedAt")
|
| 287 |
+
.ValueGeneratedOnAdd()
|
| 288 |
+
.HasColumnType("datetime")
|
| 289 |
+
.HasDefaultValueSql("(getdate())");
|
| 290 |
+
|
| 291 |
+
b.Property<string>("Email")
|
| 292 |
+
.IsRequired()
|
| 293 |
+
.HasMaxLength(100)
|
| 294 |
+
.HasColumnType("nvarchar(100)");
|
| 295 |
+
|
| 296 |
+
b.Property<string>("FcmToken")
|
| 297 |
+
.HasMaxLength(500)
|
| 298 |
+
.HasColumnType("nvarchar(500)");
|
| 299 |
+
|
| 300 |
+
b.Property<string>("FullName")
|
| 301 |
+
.IsRequired()
|
| 302 |
+
.HasMaxLength(100)
|
| 303 |
+
.HasColumnType("nvarchar(100)");
|
| 304 |
+
|
| 305 |
+
b.Property<bool>("IsActive")
|
| 306 |
+
.ValueGeneratedOnAdd()
|
| 307 |
+
.HasColumnType("bit")
|
| 308 |
+
.HasDefaultValue(true);
|
| 309 |
+
|
| 310 |
+
b.Property<string>("PasswordHash")
|
| 311 |
+
.IsRequired()
|
| 312 |
+
.HasColumnType("nvarchar(max)");
|
| 313 |
+
|
| 314 |
+
b.Property<string>("PhoneNumber")
|
| 315 |
+
.HasMaxLength(20)
|
| 316 |
+
.HasColumnType("nvarchar(20)");
|
| 317 |
+
|
| 318 |
+
b.Property<string>("Role")
|
| 319 |
+
.IsRequired()
|
| 320 |
+
.ValueGeneratedOnAdd()
|
| 321 |
+
.HasMaxLength(20)
|
| 322 |
+
.HasColumnType("nvarchar(20)")
|
| 323 |
+
.HasDefaultValue("User");
|
| 324 |
+
|
| 325 |
+
b.Property<string>("StudentId")
|
| 326 |
+
.HasMaxLength(20)
|
| 327 |
+
.HasColumnType("nvarchar(20)");
|
| 328 |
+
|
| 329 |
+
b.Property<DateTime?>("SuspensionEndDate")
|
| 330 |
+
.HasColumnType("datetime");
|
| 331 |
+
|
| 332 |
+
b.Property<DateTime?>("UpdatedAt")
|
| 333 |
+
.HasColumnType("datetime");
|
| 334 |
+
|
| 335 |
+
b.HasKey("Id");
|
| 336 |
+
|
| 337 |
+
b.HasIndex(new[] { "Email" }, "UQ_Users_Email")
|
| 338 |
+
.IsUnique();
|
| 339 |
+
|
| 340 |
+
b.ToTable("Users");
|
| 341 |
+
});
|
| 342 |
+
|
| 343 |
+
modelBuilder.Entity("DbConnect.Entities.UserSubscription", b =>
|
| 344 |
+
{
|
| 345 |
+
b.Property<Guid>("Id")
|
| 346 |
+
.ValueGeneratedOnAdd()
|
| 347 |
+
.HasColumnType("uniqueidentifier")
|
| 348 |
+
.HasDefaultValueSql("(newid())");
|
| 349 |
+
|
| 350 |
+
b.Property<DateTime>("ExpiryDate")
|
| 351 |
+
.HasColumnType("datetime");
|
| 352 |
+
|
| 353 |
+
b.Property<string>("ExternalRedemptionId")
|
| 354 |
+
.HasMaxLength(100)
|
| 355 |
+
.HasColumnType("nvarchar(100)");
|
| 356 |
+
|
| 357 |
+
b.Property<bool>("IsActive")
|
| 358 |
+
.ValueGeneratedOnAdd()
|
| 359 |
+
.HasColumnType("bit")
|
| 360 |
+
.HasDefaultValue(true);
|
| 361 |
+
|
| 362 |
+
b.Property<int>("MembershipId")
|
| 363 |
+
.HasColumnType("int");
|
| 364 |
+
|
| 365 |
+
b.Property<DateTime>("StartDate")
|
| 366 |
+
.HasColumnType("datetime");
|
| 367 |
+
|
| 368 |
+
b.Property<string>("Status")
|
| 369 |
+
.IsRequired()
|
| 370 |
+
.ValueGeneratedOnAdd()
|
| 371 |
+
.HasMaxLength(20)
|
| 372 |
+
.HasColumnType("nvarchar(20)")
|
| 373 |
+
.HasDefaultValue("Active");
|
| 374 |
+
|
| 375 |
+
b.Property<Guid>("UserId")
|
| 376 |
+
.HasColumnType("uniqueidentifier");
|
| 377 |
+
|
| 378 |
+
b.HasKey("Id");
|
| 379 |
+
|
| 380 |
+
b.HasIndex("MembershipId");
|
| 381 |
+
|
| 382 |
+
b.HasIndex("UserId");
|
| 383 |
+
|
| 384 |
+
b.ToTable("UserSubscriptions");
|
| 385 |
+
});
|
| 386 |
+
|
| 387 |
+
modelBuilder.Entity("DbConnect.Entities.WishlistItem", b =>
|
| 388 |
+
{
|
| 389 |
+
b.Property<int>("Id")
|
| 390 |
+
.ValueGeneratedOnAdd()
|
| 391 |
+
.HasColumnType("int");
|
| 392 |
+
|
| 393 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 394 |
+
|
| 395 |
+
b.Property<DateTime>("AddedAt")
|
| 396 |
+
.ValueGeneratedOnAdd()
|
| 397 |
+
.HasColumnType("datetime")
|
| 398 |
+
.HasDefaultValueSql("(getdate())");
|
| 399 |
+
|
| 400 |
+
b.Property<int>("BookId")
|
| 401 |
+
.HasColumnType("int");
|
| 402 |
+
|
| 403 |
+
b.Property<Guid>("UserId")
|
| 404 |
+
.HasColumnType("uniqueidentifier");
|
| 405 |
+
|
| 406 |
+
b.HasKey("Id");
|
| 407 |
+
|
| 408 |
+
b.HasIndex("BookId");
|
| 409 |
+
|
| 410 |
+
b.HasIndex("UserId");
|
| 411 |
+
|
| 412 |
+
b.ToTable("WishlistItems");
|
| 413 |
+
});
|
| 414 |
+
|
| 415 |
+
modelBuilder.Entity("BookCategory", b =>
|
| 416 |
+
{
|
| 417 |
+
b.HasOne("DbConnect.Entities.Book", null)
|
| 418 |
+
.WithMany()
|
| 419 |
+
.HasForeignKey("BookId")
|
| 420 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 421 |
+
.IsRequired()
|
| 422 |
+
.HasConstraintName("FK_BookCategories_Books");
|
| 423 |
+
|
| 424 |
+
b.HasOne("DbConnect.Entities.Category", null)
|
| 425 |
+
.WithMany()
|
| 426 |
+
.HasForeignKey("CategoryId")
|
| 427 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 428 |
+
.IsRequired()
|
| 429 |
+
.HasConstraintName("FK_BookCategories_Categories");
|
| 430 |
+
});
|
| 431 |
+
|
| 432 |
+
modelBuilder.Entity("DbConnect.Entities.Borrowing", b =>
|
| 433 |
+
{
|
| 434 |
+
b.HasOne("DbConnect.Entities.Book", "Book")
|
| 435 |
+
.WithMany("Borrowings")
|
| 436 |
+
.HasForeignKey("BookId")
|
| 437 |
+
.IsRequired()
|
| 438 |
+
.HasConstraintName("FK_Borrowings_Books");
|
| 439 |
+
|
| 440 |
+
b.HasOne("DbConnect.Entities.User", "User")
|
| 441 |
+
.WithMany("Borrowings")
|
| 442 |
+
.HasForeignKey("UserId")
|
| 443 |
+
.IsRequired()
|
| 444 |
+
.HasConstraintName("FK_Borrowings_Users");
|
| 445 |
+
|
| 446 |
+
b.Navigation("Book");
|
| 447 |
+
|
| 448 |
+
b.Navigation("User");
|
| 449 |
+
});
|
| 450 |
+
|
| 451 |
+
modelBuilder.Entity("DbConnect.Entities.Notification", b =>
|
| 452 |
+
{
|
| 453 |
+
b.HasOne("DbConnect.Entities.User", "User")
|
| 454 |
+
.WithMany()
|
| 455 |
+
.HasForeignKey("UserId")
|
| 456 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 457 |
+
.IsRequired()
|
| 458 |
+
.HasConstraintName("FK_Notifications_Users");
|
| 459 |
+
|
| 460 |
+
b.Navigation("User");
|
| 461 |
+
});
|
| 462 |
+
|
| 463 |
+
modelBuilder.Entity("DbConnect.Entities.UserSubscription", b =>
|
| 464 |
+
{
|
| 465 |
+
b.HasOne("DbConnect.Entities.Membership", "Membership")
|
| 466 |
+
.WithMany("UserSubscriptions")
|
| 467 |
+
.HasForeignKey("MembershipId")
|
| 468 |
+
.IsRequired()
|
| 469 |
+
.HasConstraintName("FK_UserSubscriptions_Memberships");
|
| 470 |
+
|
| 471 |
+
b.HasOne("DbConnect.Entities.User", "User")
|
| 472 |
+
.WithMany("UserSubscriptions")
|
| 473 |
+
.HasForeignKey("UserId")
|
| 474 |
+
.IsRequired()
|
| 475 |
+
.HasConstraintName("FK_UserSubscriptions_Users");
|
| 476 |
+
|
| 477 |
+
b.Navigation("Membership");
|
| 478 |
+
|
| 479 |
+
b.Navigation("User");
|
| 480 |
+
});
|
| 481 |
+
|
| 482 |
+
modelBuilder.Entity("DbConnect.Entities.WishlistItem", b =>
|
| 483 |
+
{
|
| 484 |
+
b.HasOne("DbConnect.Entities.Book", "Book")
|
| 485 |
+
.WithMany()
|
| 486 |
+
.HasForeignKey("BookId")
|
| 487 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 488 |
+
.IsRequired()
|
| 489 |
+
.HasConstraintName("FK_WishlistItems_Books");
|
| 490 |
+
|
| 491 |
+
b.HasOne("DbConnect.Entities.User", "User")
|
| 492 |
+
.WithMany()
|
| 493 |
+
.HasForeignKey("UserId")
|
| 494 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 495 |
+
.IsRequired()
|
| 496 |
+
.HasConstraintName("FK_WishlistItems_Users");
|
| 497 |
+
|
| 498 |
+
b.Navigation("Book");
|
| 499 |
+
|
| 500 |
+
b.Navigation("User");
|
| 501 |
+
});
|
| 502 |
+
|
| 503 |
+
modelBuilder.Entity("DbConnect.Entities.Book", b =>
|
| 504 |
+
{
|
| 505 |
+
b.Navigation("Borrowings");
|
| 506 |
+
});
|
| 507 |
+
|
| 508 |
+
modelBuilder.Entity("DbConnect.Entities.Membership", b =>
|
| 509 |
+
{
|
| 510 |
+
b.Navigation("UserSubscriptions");
|
| 511 |
+
});
|
| 512 |
+
|
| 513 |
+
modelBuilder.Entity("DbConnect.Entities.User", b =>
|
| 514 |
+
{
|
| 515 |
+
b.Navigation("Borrowings");
|
| 516 |
+
|
| 517 |
+
b.Navigation("UserSubscriptions");
|
| 518 |
+
});
|
| 519 |
+
#pragma warning restore 612, 618
|
| 520 |
+
}
|
| 521 |
+
}
|
| 522 |
+
}
|
DbConnect/Migrations/20260427071940_AddWishlistItems.cs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using System;
|
| 2 |
+
using Microsoft.EntityFrameworkCore.Migrations;
|
| 3 |
+
|
| 4 |
+
#nullable disable
|
| 5 |
+
|
| 6 |
+
namespace DbConnect.Migrations
|
| 7 |
+
{
|
| 8 |
+
/// <inheritdoc />
|
| 9 |
+
public partial class AddWishlistItems : Migration
|
| 10 |
+
{
|
| 11 |
+
/// <inheritdoc />
|
| 12 |
+
protected override void Up(MigrationBuilder migrationBuilder)
|
| 13 |
+
{
|
| 14 |
+
migrationBuilder.CreateTable(
|
| 15 |
+
name: "WishlistItems",
|
| 16 |
+
columns: table => new
|
| 17 |
+
{
|
| 18 |
+
Id = table.Column<int>(type: "int", nullable: false)
|
| 19 |
+
.Annotation("SqlServer:Identity", "1, 1"),
|
| 20 |
+
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
| 21 |
+
BookId = table.Column<int>(type: "int", nullable: false),
|
| 22 |
+
AddedAt = table.Column<DateTime>(type: "datetime", nullable: false, defaultValueSql: "(getdate())")
|
| 23 |
+
},
|
| 24 |
+
constraints: table =>
|
| 25 |
+
{
|
| 26 |
+
table.PrimaryKey("PK_WishlistItems", x => x.Id);
|
| 27 |
+
table.ForeignKey(
|
| 28 |
+
name: "FK_WishlistItems_Books",
|
| 29 |
+
column: x => x.BookId,
|
| 30 |
+
principalTable: "Books",
|
| 31 |
+
principalColumn: "Id",
|
| 32 |
+
onDelete: ReferentialAction.Cascade);
|
| 33 |
+
table.ForeignKey(
|
| 34 |
+
name: "FK_WishlistItems_Users",
|
| 35 |
+
column: x => x.UserId,
|
| 36 |
+
principalTable: "Users",
|
| 37 |
+
principalColumn: "Id",
|
| 38 |
+
onDelete: ReferentialAction.Cascade);
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
migrationBuilder.CreateIndex(
|
| 42 |
+
name: "IX_WishlistItems_BookId",
|
| 43 |
+
table: "WishlistItems",
|
| 44 |
+
column: "BookId");
|
| 45 |
+
|
| 46 |
+
migrationBuilder.CreateIndex(
|
| 47 |
+
name: "IX_WishlistItems_UserId",
|
| 48 |
+
table: "WishlistItems",
|
| 49 |
+
column: "UserId");
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/// <inheritdoc />
|
| 53 |
+
protected override void Down(MigrationBuilder migrationBuilder)
|
| 54 |
+
{
|
| 55 |
+
migrationBuilder.DropTable(
|
| 56 |
+
name: "WishlistItems");
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
}
|
DbConnect/Migrations/AppDbContextModelSnapshot.cs
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// <auto-generated />
|
| 2 |
+
using System;
|
| 3 |
+
using DbConnect.Data;
|
| 4 |
+
using Microsoft.EntityFrameworkCore;
|
| 5 |
+
using Microsoft.EntityFrameworkCore.Infrastructure;
|
| 6 |
+
using Microsoft.EntityFrameworkCore.Metadata;
|
| 7 |
+
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
| 8 |
+
|
| 9 |
+
#nullable disable
|
| 10 |
+
|
| 11 |
+
namespace DbConnect.Migrations
|
| 12 |
+
{
|
| 13 |
+
[DbContext(typeof(AppDbContext))]
|
| 14 |
+
partial class AppDbContextModelSnapshot : ModelSnapshot
|
| 15 |
+
{
|
| 16 |
+
protected override void BuildModel(ModelBuilder modelBuilder)
|
| 17 |
+
{
|
| 18 |
+
#pragma warning disable 612, 618
|
| 19 |
+
modelBuilder
|
| 20 |
+
.HasAnnotation("ProductVersion", "9.0.0")
|
| 21 |
+
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
| 22 |
+
|
| 23 |
+
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
| 24 |
+
|
| 25 |
+
modelBuilder.Entity("BookCategory", b =>
|
| 26 |
+
{
|
| 27 |
+
b.Property<int>("BookId")
|
| 28 |
+
.HasColumnType("int");
|
| 29 |
+
|
| 30 |
+
b.Property<int>("CategoryId")
|
| 31 |
+
.HasColumnType("int");
|
| 32 |
+
|
| 33 |
+
b.HasKey("BookId", "CategoryId")
|
| 34 |
+
.HasName("PK__BookCate__9C7051A74E7D221D");
|
| 35 |
+
|
| 36 |
+
b.HasIndex("CategoryId");
|
| 37 |
+
|
| 38 |
+
b.ToTable("BookCategories", (string)null);
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
modelBuilder.Entity("DbConnect.Entities.Book", b =>
|
| 42 |
+
{
|
| 43 |
+
b.Property<int>("Id")
|
| 44 |
+
.ValueGeneratedOnAdd()
|
| 45 |
+
.HasColumnType("int");
|
| 46 |
+
|
| 47 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 48 |
+
|
| 49 |
+
b.Property<string>("Author")
|
| 50 |
+
.IsRequired()
|
| 51 |
+
.HasMaxLength(100)
|
| 52 |
+
.HasColumnType("nvarchar(100)");
|
| 53 |
+
|
| 54 |
+
b.Property<int>("AvailableCopies")
|
| 55 |
+
.ValueGeneratedOnAdd()
|
| 56 |
+
.HasColumnType("int")
|
| 57 |
+
.HasDefaultValue(1);
|
| 58 |
+
|
| 59 |
+
b.Property<string>("CoverUrl")
|
| 60 |
+
.HasColumnType("nvarchar(max)");
|
| 61 |
+
|
| 62 |
+
b.Property<DateTime>("CreatedAt")
|
| 63 |
+
.ValueGeneratedOnAdd()
|
| 64 |
+
.HasColumnType("datetime")
|
| 65 |
+
.HasDefaultValueSql("(getdate())");
|
| 66 |
+
|
| 67 |
+
b.Property<string>("Description")
|
| 68 |
+
.HasColumnType("nvarchar(max)");
|
| 69 |
+
|
| 70 |
+
b.Property<bool>("IsActive")
|
| 71 |
+
.ValueGeneratedOnAdd()
|
| 72 |
+
.HasColumnType("bit")
|
| 73 |
+
.HasDefaultValue(true);
|
| 74 |
+
|
| 75 |
+
b.Property<string>("Isbn")
|
| 76 |
+
.IsRequired()
|
| 77 |
+
.HasMaxLength(20)
|
| 78 |
+
.HasColumnType("nvarchar(20)")
|
| 79 |
+
.HasColumnName("ISBN");
|
| 80 |
+
|
| 81 |
+
b.Property<string>("Status")
|
| 82 |
+
.IsRequired()
|
| 83 |
+
.ValueGeneratedOnAdd()
|
| 84 |
+
.HasMaxLength(20)
|
| 85 |
+
.HasColumnType("nvarchar(20)")
|
| 86 |
+
.HasDefaultValue("Available");
|
| 87 |
+
|
| 88 |
+
b.Property<string>("Title")
|
| 89 |
+
.IsRequired()
|
| 90 |
+
.HasMaxLength(200)
|
| 91 |
+
.HasColumnType("nvarchar(200)");
|
| 92 |
+
|
| 93 |
+
b.Property<int>("TotalCopies")
|
| 94 |
+
.ValueGeneratedOnAdd()
|
| 95 |
+
.HasColumnType("int")
|
| 96 |
+
.HasDefaultValue(1);
|
| 97 |
+
|
| 98 |
+
b.Property<DateTime?>("UpdatedAt")
|
| 99 |
+
.HasColumnType("datetime");
|
| 100 |
+
|
| 101 |
+
b.HasKey("Id")
|
| 102 |
+
.HasName("PK__Books__3214EC07AE1974C8");
|
| 103 |
+
|
| 104 |
+
b.HasIndex(new[] { "Isbn" }, "UQ__Books__447D36EA79CF66CE")
|
| 105 |
+
.IsUnique();
|
| 106 |
+
|
| 107 |
+
b.ToTable("Books");
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
modelBuilder.Entity("DbConnect.Entities.Borrowing", b =>
|
| 111 |
+
{
|
| 112 |
+
b.Property<Guid>("Id")
|
| 113 |
+
.ValueGeneratedOnAdd()
|
| 114 |
+
.HasColumnType("uniqueidentifier")
|
| 115 |
+
.HasDefaultValueSql("(newid())");
|
| 116 |
+
|
| 117 |
+
b.Property<int>("BookId")
|
| 118 |
+
.HasColumnType("int");
|
| 119 |
+
|
| 120 |
+
b.Property<DateTime>("BorrowDate")
|
| 121 |
+
.ValueGeneratedOnAdd()
|
| 122 |
+
.HasColumnType("datetime")
|
| 123 |
+
.HasDefaultValueSql("(getdate())");
|
| 124 |
+
|
| 125 |
+
b.Property<DateTime>("DueDate")
|
| 126 |
+
.HasColumnType("datetime");
|
| 127 |
+
|
| 128 |
+
b.Property<decimal>("FineAmount")
|
| 129 |
+
.HasColumnType("decimal(10, 2)");
|
| 130 |
+
|
| 131 |
+
b.Property<bool>("IsFinePaid")
|
| 132 |
+
.HasColumnType("bit");
|
| 133 |
+
|
| 134 |
+
b.Property<DateTime?>("ReturnDate")
|
| 135 |
+
.HasColumnType("datetime");
|
| 136 |
+
|
| 137 |
+
b.Property<string>("Status")
|
| 138 |
+
.IsRequired()
|
| 139 |
+
.ValueGeneratedOnAdd()
|
| 140 |
+
.HasMaxLength(20)
|
| 141 |
+
.HasColumnType("nvarchar(20)")
|
| 142 |
+
.HasDefaultValue("Active");
|
| 143 |
+
|
| 144 |
+
b.Property<Guid>("UserId")
|
| 145 |
+
.HasColumnType("uniqueidentifier");
|
| 146 |
+
|
| 147 |
+
b.HasKey("Id");
|
| 148 |
+
|
| 149 |
+
b.HasIndex("BookId");
|
| 150 |
+
|
| 151 |
+
b.HasIndex("UserId");
|
| 152 |
+
|
| 153 |
+
b.ToTable("Borrowings");
|
| 154 |
+
});
|
| 155 |
+
|
| 156 |
+
modelBuilder.Entity("DbConnect.Entities.Category", b =>
|
| 157 |
+
{
|
| 158 |
+
b.Property<int>("Id")
|
| 159 |
+
.ValueGeneratedOnAdd()
|
| 160 |
+
.HasColumnType("int");
|
| 161 |
+
|
| 162 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 163 |
+
|
| 164 |
+
b.Property<string>("Name")
|
| 165 |
+
.IsRequired()
|
| 166 |
+
.HasMaxLength(100)
|
| 167 |
+
.HasColumnType("nvarchar(100)");
|
| 168 |
+
|
| 169 |
+
b.HasKey("Id")
|
| 170 |
+
.HasName("PK__Categori__3214EC0751B5797D");
|
| 171 |
+
|
| 172 |
+
b.HasIndex(new[] { "Name" }, "UQ__Categori__737584F622B16D3A")
|
| 173 |
+
.IsUnique();
|
| 174 |
+
|
| 175 |
+
b.ToTable("Categories");
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
modelBuilder.Entity("DbConnect.Entities.Membership", b =>
|
| 179 |
+
{
|
| 180 |
+
b.Property<int>("Id")
|
| 181 |
+
.ValueGeneratedOnAdd()
|
| 182 |
+
.HasColumnType("int");
|
| 183 |
+
|
| 184 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 185 |
+
|
| 186 |
+
b.Property<int>("BorrowingDays")
|
| 187 |
+
.HasColumnType("int");
|
| 188 |
+
|
| 189 |
+
b.Property<int>("DurationMonths")
|
| 190 |
+
.HasColumnType("int");
|
| 191 |
+
|
| 192 |
+
b.Property<int>("MaxBooks")
|
| 193 |
+
.HasColumnType("int");
|
| 194 |
+
|
| 195 |
+
b.Property<decimal>("Price")
|
| 196 |
+
.HasColumnType("decimal(10, 2)");
|
| 197 |
+
|
| 198 |
+
b.Property<string>("RewardId")
|
| 199 |
+
.HasMaxLength(100)
|
| 200 |
+
.HasColumnType("nvarchar(100)");
|
| 201 |
+
|
| 202 |
+
b.Property<string>("Type")
|
| 203 |
+
.IsRequired()
|
| 204 |
+
.HasMaxLength(50)
|
| 205 |
+
.HasColumnType("nvarchar(50)");
|
| 206 |
+
|
| 207 |
+
b.HasKey("Id")
|
| 208 |
+
.HasName("PK__Membersh__3214EC07A6F5A55F");
|
| 209 |
+
|
| 210 |
+
b.HasIndex(new[] { "Type" }, "IX_Memberships_Type")
|
| 211 |
+
.IsUnique();
|
| 212 |
+
|
| 213 |
+
b.ToTable("Memberships");
|
| 214 |
+
});
|
| 215 |
+
|
| 216 |
+
modelBuilder.Entity("DbConnect.Entities.Notification", b =>
|
| 217 |
+
{
|
| 218 |
+
b.Property<int>("Id")
|
| 219 |
+
.ValueGeneratedOnAdd()
|
| 220 |
+
.HasColumnType("int");
|
| 221 |
+
|
| 222 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 223 |
+
|
| 224 |
+
b.Property<string>("ActionLink")
|
| 225 |
+
.HasMaxLength(500)
|
| 226 |
+
.HasColumnType("nvarchar(500)");
|
| 227 |
+
|
| 228 |
+
b.Property<string>("ActionText")
|
| 229 |
+
.HasMaxLength(100)
|
| 230 |
+
.HasColumnType("nvarchar(100)");
|
| 231 |
+
|
| 232 |
+
b.Property<DateTime>("CreatedAt")
|
| 233 |
+
.ValueGeneratedOnAdd()
|
| 234 |
+
.HasColumnType("datetime")
|
| 235 |
+
.HasDefaultValueSql("(getdate())");
|
| 236 |
+
|
| 237 |
+
b.Property<bool>("IsRead")
|
| 238 |
+
.ValueGeneratedOnAdd()
|
| 239 |
+
.HasColumnType("bit")
|
| 240 |
+
.HasDefaultValue(false);
|
| 241 |
+
|
| 242 |
+
b.Property<string>("Message")
|
| 243 |
+
.IsRequired()
|
| 244 |
+
.HasMaxLength(1000)
|
| 245 |
+
.HasColumnType("nvarchar(1000)");
|
| 246 |
+
|
| 247 |
+
b.Property<string>("Title")
|
| 248 |
+
.IsRequired()
|
| 249 |
+
.HasMaxLength(200)
|
| 250 |
+
.HasColumnType("nvarchar(200)");
|
| 251 |
+
|
| 252 |
+
b.Property<string>("Type")
|
| 253 |
+
.IsRequired()
|
| 254 |
+
.ValueGeneratedOnAdd()
|
| 255 |
+
.HasMaxLength(50)
|
| 256 |
+
.HasColumnType("nvarchar(50)")
|
| 257 |
+
.HasDefaultValue("Info");
|
| 258 |
+
|
| 259 |
+
b.Property<Guid>("UserId")
|
| 260 |
+
.HasColumnType("uniqueidentifier");
|
| 261 |
+
|
| 262 |
+
b.HasKey("Id");
|
| 263 |
+
|
| 264 |
+
b.HasIndex("UserId");
|
| 265 |
+
|
| 266 |
+
b.ToTable("Notifications");
|
| 267 |
+
});
|
| 268 |
+
|
| 269 |
+
modelBuilder.Entity("DbConnect.Entities.User", b =>
|
| 270 |
+
{
|
| 271 |
+
b.Property<Guid>("Id")
|
| 272 |
+
.ValueGeneratedOnAdd()
|
| 273 |
+
.HasColumnType("uniqueidentifier")
|
| 274 |
+
.HasDefaultValueSql("(newid())");
|
| 275 |
+
|
| 276 |
+
b.Property<string>("Address")
|
| 277 |
+
.HasMaxLength(255)
|
| 278 |
+
.HasColumnType("nvarchar(255)");
|
| 279 |
+
|
| 280 |
+
b.Property<bool?>("BanStatus")
|
| 281 |
+
.HasColumnType("bit");
|
| 282 |
+
|
| 283 |
+
b.Property<DateTime>("CreatedAt")
|
| 284 |
+
.ValueGeneratedOnAdd()
|
| 285 |
+
.HasColumnType("datetime")
|
| 286 |
+
.HasDefaultValueSql("(getdate())");
|
| 287 |
+
|
| 288 |
+
b.Property<string>("Email")
|
| 289 |
+
.IsRequired()
|
| 290 |
+
.HasMaxLength(100)
|
| 291 |
+
.HasColumnType("nvarchar(100)");
|
| 292 |
+
|
| 293 |
+
b.Property<string>("FcmToken")
|
| 294 |
+
.HasMaxLength(500)
|
| 295 |
+
.HasColumnType("nvarchar(500)");
|
| 296 |
+
|
| 297 |
+
b.Property<string>("FullName")
|
| 298 |
+
.IsRequired()
|
| 299 |
+
.HasMaxLength(100)
|
| 300 |
+
.HasColumnType("nvarchar(100)");
|
| 301 |
+
|
| 302 |
+
b.Property<bool>("IsActive")
|
| 303 |
+
.ValueGeneratedOnAdd()
|
| 304 |
+
.HasColumnType("bit")
|
| 305 |
+
.HasDefaultValue(true);
|
| 306 |
+
|
| 307 |
+
b.Property<string>("PasswordHash")
|
| 308 |
+
.IsRequired()
|
| 309 |
+
.HasColumnType("nvarchar(max)");
|
| 310 |
+
|
| 311 |
+
b.Property<string>("PhoneNumber")
|
| 312 |
+
.HasMaxLength(20)
|
| 313 |
+
.HasColumnType("nvarchar(20)");
|
| 314 |
+
|
| 315 |
+
b.Property<string>("Role")
|
| 316 |
+
.IsRequired()
|
| 317 |
+
.ValueGeneratedOnAdd()
|
| 318 |
+
.HasMaxLength(20)
|
| 319 |
+
.HasColumnType("nvarchar(20)")
|
| 320 |
+
.HasDefaultValue("User");
|
| 321 |
+
|
| 322 |
+
b.Property<string>("StudentId")
|
| 323 |
+
.HasMaxLength(20)
|
| 324 |
+
.HasColumnType("nvarchar(20)");
|
| 325 |
+
|
| 326 |
+
b.Property<DateTime?>("SuspensionEndDate")
|
| 327 |
+
.HasColumnType("datetime");
|
| 328 |
+
|
| 329 |
+
b.Property<DateTime?>("UpdatedAt")
|
| 330 |
+
.HasColumnType("datetime");
|
| 331 |
+
|
| 332 |
+
b.HasKey("Id");
|
| 333 |
+
|
| 334 |
+
b.HasIndex(new[] { "Email" }, "UQ_Users_Email")
|
| 335 |
+
.IsUnique();
|
| 336 |
+
|
| 337 |
+
b.ToTable("Users");
|
| 338 |
+
});
|
| 339 |
+
|
| 340 |
+
modelBuilder.Entity("DbConnect.Entities.UserSubscription", b =>
|
| 341 |
+
{
|
| 342 |
+
b.Property<Guid>("Id")
|
| 343 |
+
.ValueGeneratedOnAdd()
|
| 344 |
+
.HasColumnType("uniqueidentifier")
|
| 345 |
+
.HasDefaultValueSql("(newid())");
|
| 346 |
+
|
| 347 |
+
b.Property<DateTime>("ExpiryDate")
|
| 348 |
+
.HasColumnType("datetime");
|
| 349 |
+
|
| 350 |
+
b.Property<string>("ExternalRedemptionId")
|
| 351 |
+
.HasMaxLength(100)
|
| 352 |
+
.HasColumnType("nvarchar(100)");
|
| 353 |
+
|
| 354 |
+
b.Property<bool>("IsActive")
|
| 355 |
+
.ValueGeneratedOnAdd()
|
| 356 |
+
.HasColumnType("bit")
|
| 357 |
+
.HasDefaultValue(true);
|
| 358 |
+
|
| 359 |
+
b.Property<int>("MembershipId")
|
| 360 |
+
.HasColumnType("int");
|
| 361 |
+
|
| 362 |
+
b.Property<DateTime>("StartDate")
|
| 363 |
+
.HasColumnType("datetime");
|
| 364 |
+
|
| 365 |
+
b.Property<string>("Status")
|
| 366 |
+
.IsRequired()
|
| 367 |
+
.ValueGeneratedOnAdd()
|
| 368 |
+
.HasMaxLength(20)
|
| 369 |
+
.HasColumnType("nvarchar(20)")
|
| 370 |
+
.HasDefaultValue("Active");
|
| 371 |
+
|
| 372 |
+
b.Property<Guid>("UserId")
|
| 373 |
+
.HasColumnType("uniqueidentifier");
|
| 374 |
+
|
| 375 |
+
b.HasKey("Id");
|
| 376 |
+
|
| 377 |
+
b.HasIndex("MembershipId");
|
| 378 |
+
|
| 379 |
+
b.HasIndex("UserId");
|
| 380 |
+
|
| 381 |
+
b.ToTable("UserSubscriptions");
|
| 382 |
+
});
|
| 383 |
+
|
| 384 |
+
modelBuilder.Entity("DbConnect.Entities.WishlistItem", b =>
|
| 385 |
+
{
|
| 386 |
+
b.Property<int>("Id")
|
| 387 |
+
.ValueGeneratedOnAdd()
|
| 388 |
+
.HasColumnType("int");
|
| 389 |
+
|
| 390 |
+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
| 391 |
+
|
| 392 |
+
b.Property<DateTime>("AddedAt")
|
| 393 |
+
.ValueGeneratedOnAdd()
|
| 394 |
+
.HasColumnType("datetime")
|
| 395 |
+
.HasDefaultValueSql("(getdate())");
|
| 396 |
+
|
| 397 |
+
b.Property<int>("BookId")
|
| 398 |
+
.HasColumnType("int");
|
| 399 |
+
|
| 400 |
+
b.Property<Guid>("UserId")
|
| 401 |
+
.HasColumnType("uniqueidentifier");
|
| 402 |
+
|
| 403 |
+
b.HasKey("Id");
|
| 404 |
+
|
| 405 |
+
b.HasIndex("BookId");
|
| 406 |
+
|
| 407 |
+
b.HasIndex("UserId");
|
| 408 |
+
|
| 409 |
+
b.ToTable("WishlistItems");
|
| 410 |
+
});
|
| 411 |
+
|
| 412 |
+
modelBuilder.Entity("BookCategory", b =>
|
| 413 |
+
{
|
| 414 |
+
b.HasOne("DbConnect.Entities.Book", null)
|
| 415 |
+
.WithMany()
|
| 416 |
+
.HasForeignKey("BookId")
|
| 417 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 418 |
+
.IsRequired()
|
| 419 |
+
.HasConstraintName("FK_BookCategories_Books");
|
| 420 |
+
|
| 421 |
+
b.HasOne("DbConnect.Entities.Category", null)
|
| 422 |
+
.WithMany()
|
| 423 |
+
.HasForeignKey("CategoryId")
|
| 424 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 425 |
+
.IsRequired()
|
| 426 |
+
.HasConstraintName("FK_BookCategories_Categories");
|
| 427 |
+
});
|
| 428 |
+
|
| 429 |
+
modelBuilder.Entity("DbConnect.Entities.Borrowing", b =>
|
| 430 |
+
{
|
| 431 |
+
b.HasOne("DbConnect.Entities.Book", "Book")
|
| 432 |
+
.WithMany("Borrowings")
|
| 433 |
+
.HasForeignKey("BookId")
|
| 434 |
+
.IsRequired()
|
| 435 |
+
.HasConstraintName("FK_Borrowings_Books");
|
| 436 |
+
|
| 437 |
+
b.HasOne("DbConnect.Entities.User", "User")
|
| 438 |
+
.WithMany("Borrowings")
|
| 439 |
+
.HasForeignKey("UserId")
|
| 440 |
+
.IsRequired()
|
| 441 |
+
.HasConstraintName("FK_Borrowings_Users");
|
| 442 |
+
|
| 443 |
+
b.Navigation("Book");
|
| 444 |
+
|
| 445 |
+
b.Navigation("User");
|
| 446 |
+
});
|
| 447 |
+
|
| 448 |
+
modelBuilder.Entity("DbConnect.Entities.Notification", b =>
|
| 449 |
+
{
|
| 450 |
+
b.HasOne("DbConnect.Entities.User", "User")
|
| 451 |
+
.WithMany()
|
| 452 |
+
.HasForeignKey("UserId")
|
| 453 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 454 |
+
.IsRequired()
|
| 455 |
+
.HasConstraintName("FK_Notifications_Users");
|
| 456 |
+
|
| 457 |
+
b.Navigation("User");
|
| 458 |
+
});
|
| 459 |
+
|
| 460 |
+
modelBuilder.Entity("DbConnect.Entities.UserSubscription", b =>
|
| 461 |
+
{
|
| 462 |
+
b.HasOne("DbConnect.Entities.Membership", "Membership")
|
| 463 |
+
.WithMany("UserSubscriptions")
|
| 464 |
+
.HasForeignKey("MembershipId")
|
| 465 |
+
.IsRequired()
|
| 466 |
+
.HasConstraintName("FK_UserSubscriptions_Memberships");
|
| 467 |
+
|
| 468 |
+
b.HasOne("DbConnect.Entities.User", "User")
|
| 469 |
+
.WithMany("UserSubscriptions")
|
| 470 |
+
.HasForeignKey("UserId")
|
| 471 |
+
.IsRequired()
|
| 472 |
+
.HasConstraintName("FK_UserSubscriptions_Users");
|
| 473 |
+
|
| 474 |
+
b.Navigation("Membership");
|
| 475 |
+
|
| 476 |
+
b.Navigation("User");
|
| 477 |
+
});
|
| 478 |
+
|
| 479 |
+
modelBuilder.Entity("DbConnect.Entities.WishlistItem", b =>
|
| 480 |
+
{
|
| 481 |
+
b.HasOne("DbConnect.Entities.Book", "Book")
|
| 482 |
+
.WithMany()
|
| 483 |
+
.HasForeignKey("BookId")
|
| 484 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 485 |
+
.IsRequired()
|
| 486 |
+
.HasConstraintName("FK_WishlistItems_Books");
|
| 487 |
+
|
| 488 |
+
b.HasOne("DbConnect.Entities.User", "User")
|
| 489 |
+
.WithMany()
|
| 490 |
+
.HasForeignKey("UserId")
|
| 491 |
+
.OnDelete(DeleteBehavior.Cascade)
|
| 492 |
+
.IsRequired()
|
| 493 |
+
.HasConstraintName("FK_WishlistItems_Users");
|
| 494 |
+
|
| 495 |
+
b.Navigation("Book");
|
| 496 |
+
|
| 497 |
+
b.Navigation("User");
|
| 498 |
+
});
|
| 499 |
+
|
| 500 |
+
modelBuilder.Entity("DbConnect.Entities.Book", b =>
|
| 501 |
+
{
|
| 502 |
+
b.Navigation("Borrowings");
|
| 503 |
+
});
|
| 504 |
+
|
| 505 |
+
modelBuilder.Entity("DbConnect.Entities.Membership", b =>
|
| 506 |
+
{
|
| 507 |
+
b.Navigation("UserSubscriptions");
|
| 508 |
+
});
|
| 509 |
+
|
| 510 |
+
modelBuilder.Entity("DbConnect.Entities.User", b =>
|
| 511 |
+
{
|
| 512 |
+
b.Navigation("Borrowings");
|
| 513 |
+
|
| 514 |
+
b.Navigation("UserSubscriptions");
|
| 515 |
+
});
|
| 516 |
+
#pragma warning restore 612, 618
|
| 517 |
+
}
|
| 518 |
+
}
|
| 519 |
+
}
|