Skip to content

Commit 7a5d1a4

Browse files
committed
Merge origin/main into main
2 parents 4bffdb2 + a57ac8d commit 7a5d1a4

File tree

6 files changed

+251
-48
lines changed

6 files changed

+251
-48
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
using LeadManagementPortal.Data;
5+
using LeadManagementPortal.Models;
6+
using LeadManagementPortal.Services;
7+
using Microsoft.EntityFrameworkCore;
8+
using Xunit;
9+
10+
namespace LeadManagementPortal.Tests
11+
{
12+
public class NotificationServiceBulkMarkTests
13+
{
14+
private static ApplicationDbContext GetInMemoryDbContext()
15+
{
16+
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
17+
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
18+
.Options;
19+
return new ApplicationDbContext(options);
20+
}
21+
22+
[Fact]
23+
public async Task MarkReadBulkAsync_UpdatesOnlyUnreadVisibleNotifications()
24+
{
25+
using var context = GetInMemoryDbContext();
26+
var svc = new NotificationService(context);
27+
28+
context.Notifications.AddRange(
29+
new Notification { Id = 1, UserId = "u1", Role = null, IsRead = false, CreatedAt = DateTime.UtcNow, Title = "t1", Message = "m", Type = "x" },
30+
new Notification { Id = 2, UserId = "u1", Role = null, IsRead = true, CreatedAt = DateTime.UtcNow, ReadAt = DateTime.UtcNow, Title = "t2", Message = "m", Type = "x" },
31+
new Notification { Id = 3, UserId = "u2", Role = null, IsRead = false, CreatedAt = DateTime.UtcNow, Title = "t3", Message = "m", Type = "x" },
32+
new Notification { Id = 4, UserId = null, Role = "SalesOrgAdmin", IsRead = false, CreatedAt = DateTime.UtcNow, Title = "t4", Message = "m", Type = "x" }
33+
);
34+
await context.SaveChangesAsync();
35+
36+
var updated = await svc.MarkReadBulkAsync(
37+
notificationIds: new[] { 0, 1, 1, 2, 3, 4, 999 },
38+
userId: "u1",
39+
role: "SalesOrgAdmin");
40+
41+
Assert.Equal(2, updated);
42+
43+
var n1 = await context.Notifications.SingleAsync(n => n.Id == 1);
44+
var n2 = await context.Notifications.SingleAsync(n => n.Id == 2);
45+
var n3 = await context.Notifications.SingleAsync(n => n.Id == 3);
46+
var n4 = await context.Notifications.SingleAsync(n => n.Id == 4);
47+
48+
Assert.True(n1.IsRead);
49+
Assert.NotNull(n1.ReadAt);
50+
51+
Assert.True(n2.IsRead);
52+
Assert.NotNull(n2.ReadAt);
53+
54+
Assert.False(n3.IsRead);
55+
Assert.Null(n3.ReadAt);
56+
57+
Assert.True(n4.IsRead);
58+
Assert.NotNull(n4.ReadAt);
59+
}
60+
61+
[Fact]
62+
public async Task MarkUnreadBulkAsync_UpdatesOnlyReadVisibleNotifications()
63+
{
64+
using var context = GetInMemoryDbContext();
65+
var svc = new NotificationService(context);
66+
67+
context.Notifications.AddRange(
68+
new Notification { Id = 10, UserId = "u1", Role = null, IsRead = true, CreatedAt = DateTime.UtcNow, ReadAt = DateTime.UtcNow, Title = "t1", Message = "m", Type = "x" },
69+
new Notification { Id = 11, UserId = "u1", Role = null, IsRead = false, CreatedAt = DateTime.UtcNow, Title = "t2", Message = "m", Type = "x" },
70+
new Notification { Id = 12, UserId = null, Role = "SalesOrgAdmin", IsRead = true, CreatedAt = DateTime.UtcNow, ReadAt = DateTime.UtcNow, Title = "t3", Message = "m", Type = "x" }
71+
);
72+
await context.SaveChangesAsync();
73+
74+
var updated = await svc.MarkUnreadBulkAsync(
75+
notificationIds: new[] { 10, 11, 12 },
76+
userId: "u1",
77+
role: "SalesOrgAdmin");
78+
79+
Assert.Equal(2, updated);
80+
81+
var unreadIds = await context.Notifications
82+
.Where(n => !n.IsRead)
83+
.Select(n => n.Id)
84+
.OrderBy(id => id)
85+
.ToListAsync();
86+
87+
Assert.Equal(new[] { 10, 11, 12 }, unreadIds);
88+
}
89+
}
90+
}
91+

LeadManagementPortal/Controllers/NotificationsApiController.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,42 @@ public async Task<IActionResult> MarkUnread([FromBody] NotificationActionRequest
9999
return Ok(new { success = true, data = new { message = "Notification marked as unread" } });
100100
}
101101

102+
// POST /api/notifications/mark_read_bulk
103+
[HttpPost("mark_read_bulk")]
104+
public async Task<IActionResult> MarkReadBulk([FromBody] NotificationBulkActionRequest request)
105+
{
106+
var ids = request?.notification_ids?
107+
.Where(id => id > 0)
108+
.Distinct()
109+
.Take(200)
110+
.ToArray() ?? Array.Empty<int>();
111+
112+
if (ids.Length == 0)
113+
return BadRequest(new { success = false, error = new { message = "notification_ids required", code = "VALIDATION_ERROR" } });
114+
115+
var (userId, role) = GetUserContext();
116+
var updated = await _notificationService.MarkReadBulkAsync(ids, userId, role);
117+
return Ok(new { success = true, data = new { requested_count = ids.Length, updated_count = updated } });
118+
}
119+
120+
// POST /api/notifications/mark_unread_bulk
121+
[HttpPost("mark_unread_bulk")]
122+
public async Task<IActionResult> MarkUnreadBulk([FromBody] NotificationBulkActionRequest request)
123+
{
124+
var ids = request?.notification_ids?
125+
.Where(id => id > 0)
126+
.Distinct()
127+
.Take(200)
128+
.ToArray() ?? Array.Empty<int>();
129+
130+
if (ids.Length == 0)
131+
return BadRequest(new { success = false, error = new { message = "notification_ids required", code = "VALIDATION_ERROR" } });
132+
133+
var (userId, role) = GetUserContext();
134+
var updated = await _notificationService.MarkUnreadBulkAsync(ids, userId, role);
135+
return Ok(new { success = true, data = new { requested_count = ids.Length, updated_count = updated } });
136+
}
137+
102138
// POST /api/notifications/mark_all_read
103139
[HttpPost("mark_all_read")]
104140
public async Task<IActionResult> MarkAllRead()
@@ -120,4 +156,9 @@ public class NotificationActionRequest
120156
{
121157
public int notification_id { get; set; }
122158
}
159+
160+
public class NotificationBulkActionRequest
161+
{
162+
public int[] notification_ids { get; set; } = Array.Empty<int>();
163+
}
123164
}

LeadManagementPortal/Services/INotificationService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ public interface INotificationService
3434
/// <summary>Mark a single notification as unread (ownership enforced).</summary>
3535
Task<bool> MarkUnreadAsync(int notificationId, string userId, string role);
3636

37+
/// <summary>Mark many notifications as read (ownership enforced). Returns count updated.</summary>
38+
Task<int> MarkReadBulkAsync(IReadOnlyCollection<int> notificationIds, string userId, string role);
39+
40+
/// <summary>Mark many notifications as unread (ownership enforced). Returns count updated.</summary>
41+
Task<int> MarkUnreadBulkAsync(IReadOnlyCollection<int> notificationIds, string userId, string role);
42+
3743
/// <summary>Mark all of a user's notifications as read.</summary>
3844
Task<bool> MarkAllReadAsync(string userId, string role);
3945

LeadManagementPortal/Services/NotificationService.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,63 @@ public async Task<bool> MarkUnreadAsync(int notificationId, string userId, strin
130130
return true;
131131
}
132132

133+
public async Task<int> MarkReadBulkAsync(IReadOnlyCollection<int> notificationIds, string userId, string role)
134+
{
135+
if (notificationIds == null || notificationIds.Count == 0) return 0;
136+
137+
var ids = notificationIds
138+
.Where(id => id > 0)
139+
.Distinct()
140+
.Take(200)
141+
.ToArray();
142+
143+
if (ids.Length == 0) return 0;
144+
145+
var notifications = await _context.Notifications
146+
.Where(n => ids.Contains(n.Id) && (n.UserId == userId || n.Role == role) && !n.IsRead)
147+
.ToListAsync();
148+
149+
if (notifications.Count == 0) return 0;
150+
151+
var now = DateTime.UtcNow;
152+
foreach (var n in notifications)
153+
{
154+
n.IsRead = true;
155+
n.ReadAt = now;
156+
}
157+
158+
await _context.SaveChangesAsync();
159+
return notifications.Count;
160+
}
161+
162+
public async Task<int> MarkUnreadBulkAsync(IReadOnlyCollection<int> notificationIds, string userId, string role)
163+
{
164+
if (notificationIds == null || notificationIds.Count == 0) return 0;
165+
166+
var ids = notificationIds
167+
.Where(id => id > 0)
168+
.Distinct()
169+
.Take(200)
170+
.ToArray();
171+
172+
if (ids.Length == 0) return 0;
173+
174+
var notifications = await _context.Notifications
175+
.Where(n => ids.Contains(n.Id) && (n.UserId == userId || n.Role == role) && n.IsRead)
176+
.ToListAsync();
177+
178+
if (notifications.Count == 0) return 0;
179+
180+
foreach (var n in notifications)
181+
{
182+
n.IsRead = false;
183+
n.ReadAt = null;
184+
}
185+
186+
await _context.SaveChangesAsync();
187+
return notifications.Count;
188+
}
189+
133190
public async Task<bool> MarkAllReadAsync(string userId, string role)
134191
{
135192
var unread = await _context.Notifications

LeadManagementPortal/Views/Shared/_Layout.cshtml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,7 @@
175175
<div id="notifDropdown">
176176
<div class="notif-header">
177177
<span class="notif-header-title">Notifications</span>
178-
<button class="notif-header-mark-all" type="button"
179-
onclick="DiRxNotifications.markAllRead()">
178+
<button class="notif-header-mark-all" id="notifMarkAllBtn" type="button">
180179
Mark all read
181180
</button>
182181
</div>

0 commit comments

Comments
 (0)