Yuyuqt commited on
Commit
ccc2569
·
1 Parent(s): cf9e5bb

add: removed firebase json file

Browse files
Files changed (37) hide show
  1. .gitignore +4 -1
  2. Backend/Backend.csproj +5 -0
  3. Backend/Features/Books/BookController.cs +10 -0
  4. Backend/Features/Books/BookService.cs +10 -0
  5. Backend/Features/Borrowing/BorrowingService.cs +30 -1
  6. Backend/Features/Notification/INotificationService.cs +13 -0
  7. Backend/Features/Notification/NotificationBackgroundService.cs +113 -0
  8. Backend/Features/Notification/NotificationModels.cs +36 -0
  9. Backend/Features/Notification/NotificationService.cs +130 -0
  10. Backend/Features/Notification/NotificationsController.cs +101 -0
  11. Backend/Features/Users/UserController.cs +20 -0
  12. Backend/Features/Users/UserService.cs +13 -0
  13. Backend/Features/Wishlist/WishlistController.cs +47 -0
  14. Backend/Features/Wishlist/WishlistService.cs +57 -0
  15. Backend/Program.cs +13 -0
  16. BlazorFrontend/BlazorFrontend.csproj +1 -0
  17. BlazorFrontend/Components/App.razor +1 -0
  18. BlazorFrontend/Components/Layout/MainLayout.razor +33 -2
  19. BlazorFrontend/Components/Pages/FcmTest.razor +78 -0
  20. BlazorFrontend/Components/Pages/Notifications.razor +158 -0
  21. BlazorFrontend/Components/Pages/Wishlist.razor +142 -131
  22. BlazorFrontend/Models/Dtos/LibraryDtos.cs +12 -0
  23. BlazorFrontend/Services/LibraryApiClient.cs +40 -0
  24. BlazorFrontend/Services/WishlistService.cs +45 -3
  25. BlazorFrontend/wwwroot/firebase-init.js +50 -0
  26. BlazorFrontend/wwwroot/firebase-messaging-sw.js +37 -0
  27. DbConnect/Data/AppDbContext.cs +45 -4
  28. DbConnect/Entities/Notification.cs +26 -0
  29. DbConnect/Entities/User.cs +3 -1
  30. DbConnect/Entities/WishlistItem.cs +18 -0
  31. DbConnect/Migrations/20260427043736_AddNotifications.Designer.cs +469 -0
  32. DbConnect/Migrations/20260427043736_AddNotifications.cs +74 -0
  33. DbConnect/Migrations/20260427052139_UpdateUserWithFcmToken.Designer.cs +473 -0
  34. DbConnect/Migrations/20260427052139_UpdateUserWithFcmToken.cs +29 -0
  35. DbConnect/Migrations/20260427071940_AddWishlistItems.Designer.cs +522 -0
  36. DbConnect/Migrations/20260427071940_AddWishlistItems.cs +59 -0
  37. 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
- <button type="button" class="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
- </button>
 
 
 
 
 
 
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
- <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">
13
- <div>
14
- <h1 class="text-3xl font-extrabold tracking-tight text-mystic-900">My Collections</h1>
15
- <p class="text-mystic-600 mt-1">Track books you want to borrow and your all-time favourites.</p>
16
- </div>
17
- </div>
 
 
18
 
19
- <div class="mb-6 border-b border-mystic-200">
20
- <nav class="-mb-px flex gap-6">
21
- <button @onclick='() => _activeTab = "wishlist"'
22
- 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")'>
23
- <span class="inline-flex items-center gap-1.5">
24
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
25
- <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" />
26
- </svg>
27
- Wishlist
28
- @if (_wishlist.Any())
29
- {
30
- <span class="ml-1 px-1.5 py-0.5 rounded-full bg-amber-500 text-white text-[10px]">@_wishlist.Count</span>
31
- }
32
- </span>
33
- </button>
34
- <button @onclick='() => _activeTab = "favourites"'
35
- 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")'>
36
- <span class="inline-flex items-center gap-1.5">
37
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
38
- <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" />
39
- </svg>
40
- Favourites
41
- @if (_favourites.Any())
42
- {
43
- <span class="ml-1 px-1.5 py-0.5 rounded-full bg-rose-500 text-white text-[10px]">@_favourites.Count</span>
44
- }
45
- </span>
46
- </button>
47
- </nav>
48
- </div>
49
 
50
- @if (_isLoading)
51
- {
52
- <div class="py-20 flex justify-center">
53
- <div class="animate-spin rounded-full h-10 w-10 border-b-2 border-mystic-900"></div>
54
- </div>
55
- }
56
- else
57
- {
58
- <div class="animate-in fade-in duration-300">
59
- @if (_activeTab == "wishlist")
60
  {
61
- @if (!_wishlist.Any())
62
- {
63
- <div class="py-20 text-center bg-mystic-50 rounded-3xl border-2 border-dashed border-mystic-200">
64
- <p class="text-mystic-400 font-medium">Your wishlist is empty. Add books that are currently out of stock!</p>
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="card p-4 flex gap-4 group hover:shadow-md transition-shadow relative">
74
- <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">
75
- @if (!string.IsNullOrWhiteSpace(book.CoverUrl))
76
- {
77
- <img src="@book.CoverUrl" alt="@book.Title" class="w-full h-full object-cover" />
78
- }
79
- else
80
- {
81
- <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>
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="absolute -top-2 -right-2 bg-emerald-500 text-white p-1 rounded-full shadow-sm animate-bounce">
101
- <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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  </div>
103
  }
104
  </div>
 
 
 
 
 
 
105
  }
106
- </div>
107
- <div class="mt-8 p-4 bg-mystic-900 rounded-2xl text-white flex items-center gap-4">
108
- <div class="p-2 bg-mystic-800 rounded-lg">
109
- <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>
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="card p-4 flex gap-4 group hover:shadow-md transition-shadow">
130
- <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">
131
- @if (!string.IsNullOrWhiteSpace(book.CoverUrl))
132
- {
133
- <img src="@book.CoverUrl" alt="@book.Title" class="w-full h-full object-cover" />
134
- }
135
- else
136
- {
137
- <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>
138
- }
139
- </div>
140
- <div class="flex-grow min-w-0">
141
- <h3 class="text-sm font-bold text-mystic-900 truncate">@book.Title</h3>
142
- <p class="text-xs text-mystic-500 mt-0.5 truncate">@book.Author</p>
143
- <div class="mt-4 flex items-center gap-2">
144
- <button @onclick='() => Navigation.NavigateTo($"/Books")' class="text-[10px] font-bold text-mystic-600 hover:text-mystic-900 uppercase tracking-tight">View Info</button>
145
- <span class="text-mystic-200">|</span>
146
- <button @onclick="() => RemoveFromFavourite(book.Id)" class="text-[10px] font-bold text-rose-600 hover:text-rose-700 uppercase tracking-tight">Remove</button>
 
 
 
 
 
 
 
 
 
 
 
 
147
  </div>
148
- </div>
149
  </div>
150
  }
151
- </div>
152
- }
153
  }
154
- </div>
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
- return JsonSerializer.Deserialize<List<BookDto>>(result.Value) ?? new List<BookDto>();
 
 
 
 
 
 
 
 
 
 
 
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
- return JsonSerializer.Deserialize<List<BookDto>>(result.Value) ?? new List<BookDto>();
 
 
 
 
 
 
 
 
 
 
 
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
- using System;
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
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
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
- using System;
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
+ }