User commited on
Commit
e40d850
·
1 Parent(s): 64a3c1d

fix: remove dashboard widgets flow

Browse files
Files changed (21) hide show
  1. TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs +0 -93
  2. TaskTrackingSystem.Database/AppDbContextModels/DashboardWidget.cs +0 -39
  3. TaskTrackingSystem.Database/AppDbContextModels/Role.cs +0 -2
  4. TaskTrackingSystem.Database/AppDbContextModels/RoleDashboardWidget.cs +0 -42
  5. TaskTrackingSystem.Database/AppDbContextModels/User.cs +0 -1
  6. TaskTrackingSystem.Database/AppDbContextModels/UserDashboardLayout.cs +0 -42
  7. TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetAdminDto.cs +0 -37
  8. TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetDto.cs +0 -49
  9. TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetLayoutItemDto.cs +0 -23
  10. TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetLayoutSaveRequestDto.cs +0 -9
  11. TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetRoleAccessDto.cs +0 -25
  12. TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetRoleAccessSaveRequestDto.cs +0 -9
  13. TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetUpsertDto.cs +0 -35
  14. TaskTrackingSystem.WebApi/Features/Dashboard/DashboardController.cs +1 -100
  15. TaskTrackingSystem.WebApi/Features/Dashboard/DashboardService.cs +3 -528
  16. TaskTrackingSystem.WebApi/Program.cs +0 -141
  17. TaskTrackingSystem.WebApp/Components/Layout/MainLayout.razor +0 -1
  18. TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor +0 -4
  19. TaskTrackingSystem.WebApp/Components/Pages/Home.razor +334 -338
  20. TaskTrackingSystem.WebApp/Components/Pages/WidgetSetup.razor +0 -800
  21. scripts/reset-role-access-ids.sql +0 -0
TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs CHANGED
@@ -24,8 +24,6 @@ public partial class AppDbContext : DbContext
24
 
25
  public virtual DbSet<Menu> Menus { get; set; }
26
 
27
- public virtual DbSet<DashboardWidget> DashboardWidgets { get; set; }
28
-
29
  public virtual DbSet<Notification> Notifications { get; set; }
30
 
31
  public virtual DbSet<Permission> Permissions { get; set; }
@@ -38,8 +36,6 @@ public partial class AppDbContext : DbContext
38
 
39
  public virtual DbSet<RoleMenu> RoleMenus { get; set; }
40
 
41
- public virtual DbSet<RoleDashboardWidget> RoleDashboardWidgets { get; set; }
42
-
43
  public virtual DbSet<RolePermission> RolePermissions { get; set; }
44
 
45
  public virtual DbSet<Task> Tasks { get; set; }
@@ -48,8 +44,6 @@ public partial class AppDbContext : DbContext
48
 
49
  public virtual DbSet<TimeLog> TimeLogs { get; set; }
50
 
51
- public virtual DbSet<UserDashboardLayout> UserDashboardLayouts { get; set; }
52
-
53
  public virtual DbSet<UserDevice> UserDevices { get; set; }
54
 
55
  public virtual DbSet<User> Users { get; set; }
@@ -142,29 +136,6 @@ public partial class AppDbContext : DbContext
142
  .HasConstraintName("FK_Menus_ParentMenu");
143
  });
144
 
145
- modelBuilder.Entity<DashboardWidget>(entity =>
146
- {
147
- entity.HasKey(e => e.WidgetId);
148
-
149
- entity.HasIndex(e => e.WidgetCode, "UQ_DashboardWidgets_WidgetCode").IsUnique();
150
-
151
- entity.Property(e => e.Category).HasMaxLength(50);
152
- entity.Property(e => e.ComponentKey).HasMaxLength(100);
153
- entity.Property(e => e.CreatedAt)
154
- .HasPrecision(0)
155
- .HasDefaultValueSql("(sysutcdatetime())");
156
- entity.Property(e => e.DataSourceKey).HasMaxLength(100);
157
- entity.Property(e => e.DefaultHeight).HasDefaultValue(3);
158
- entity.Property(e => e.DefaultOrder).HasDefaultValue(0);
159
- entity.Property(e => e.DefaultWidth).HasDefaultValue(4);
160
- entity.Property(e => e.Description).HasMaxLength(500);
161
- entity.Property(e => e.IsActive).HasDefaultValue(true);
162
- entity.Property(e => e.IsDeleted).HasDefaultValue(false);
163
- entity.Property(e => e.UpdatedAt).HasPrecision(0);
164
- entity.Property(e => e.WidgetCode).HasMaxLength(50);
165
- entity.Property(e => e.WidgetName).HasMaxLength(100);
166
- });
167
-
168
  modelBuilder.Entity<Notification>(entity =>
169
  {
170
  entity.ToTable("notifications");
@@ -289,39 +260,6 @@ public partial class AppDbContext : DbContext
289
  .HasConstraintName("FK_RoleMenus_Roles");
290
  });
291
 
292
- modelBuilder.Entity<RoleDashboardWidget>(entity =>
293
- {
294
- entity.HasKey(e => e.RoleDashboardWidgetId);
295
-
296
- entity.HasIndex(e => e.RoleId, "IX_RoleDashboardWidgets_RoleId");
297
- entity.HasIndex(e => e.WidgetId, "IX_RoleDashboardWidgets_WidgetId");
298
- entity.HasIndex(e => new { e.RoleId, e.WidgetId }, "UQ_RoleDashboardWidgets_Role_Widget").IsUnique();
299
-
300
- entity.Property(e => e.CanConfigure).HasDefaultValue(false);
301
- entity.Property(e => e.CanView).HasDefaultValue(true);
302
- entity.Property(e => e.CreatedAt)
303
- .HasPrecision(0)
304
- .HasDefaultValueSql("(sysutcdatetime())");
305
- entity.Property(e => e.DefaultGridX).HasDefaultValue(0);
306
- entity.Property(e => e.DefaultGridY).HasDefaultValue(0);
307
- entity.Property(e => e.DefaultHeight).HasDefaultValue(3);
308
- entity.Property(e => e.DefaultSortOrder).HasDefaultValue(0);
309
- entity.Property(e => e.DefaultWidth).HasDefaultValue(4);
310
- entity.Property(e => e.IsDefaultVisible).HasDefaultValue(true);
311
- entity.Property(e => e.IsDeleted).HasDefaultValue(false);
312
- entity.Property(e => e.UpdatedAt).HasPrecision(0);
313
-
314
- entity.HasOne(d => d.Role).WithMany(p => p.RoleDashboardWidgets)
315
- .HasForeignKey(d => d.RoleId)
316
- .OnDelete(DeleteBehavior.Cascade)
317
- .HasConstraintName("FK_RoleDashboardWidgets_Roles");
318
-
319
- entity.HasOne(d => d.Widget).WithMany(p => p.RoleDashboardWidgets)
320
- .HasForeignKey(d => d.WidgetId)
321
- .OnDelete(DeleteBehavior.Cascade)
322
- .HasConstraintName("FK_RoleDashboardWidgets_DashboardWidgets");
323
- });
324
-
325
  modelBuilder.Entity<RolePermission>(entity =>
326
  {
327
  entity.HasIndex(e => e.PermissionId, "IX_RolePermissions_PermissionId");
@@ -421,37 +359,6 @@ public partial class AppDbContext : DbContext
421
  .HasConstraintName("FK_TimeLogs_Users");
422
  });
423
 
424
- modelBuilder.Entity<UserDashboardLayout>(entity =>
425
- {
426
- entity.HasKey(e => e.UserDashboardLayoutId);
427
-
428
- entity.HasIndex(e => e.UserId, "IX_UserDashboardLayouts_UserId");
429
- entity.HasIndex(e => e.WidgetId, "IX_UserDashboardLayouts_WidgetId");
430
- entity.HasIndex(e => new { e.UserId, e.WidgetId }, "UQ_UserDashboardLayouts_User_Widget").IsUnique();
431
-
432
- entity.Property(e => e.ConfigJson).HasColumnType("nvarchar(max)");
433
- entity.Property(e => e.CreatedAt)
434
- .HasPrecision(0)
435
- .HasDefaultValueSql("(sysutcdatetime())");
436
- entity.Property(e => e.Height).HasDefaultValue(3);
437
- entity.Property(e => e.IsDeleted).HasDefaultValue(false);
438
- entity.Property(e => e.IsHidden).HasDefaultValue(false);
439
- entity.Property(e => e.IsPinned).HasDefaultValue(false);
440
- entity.Property(e => e.SortOrder).HasDefaultValue(0);
441
- entity.Property(e => e.UpdatedAt).HasPrecision(0);
442
- entity.Property(e => e.Width).HasDefaultValue(4);
443
-
444
- entity.HasOne(d => d.User).WithMany(p => p.UserDashboardLayouts)
445
- .HasForeignKey(d => d.UserId)
446
- .OnDelete(DeleteBehavior.Cascade)
447
- .HasConstraintName("FK_UserDashboardLayouts_Users");
448
-
449
- entity.HasOne(d => d.Widget).WithMany(p => p.UserDashboardLayouts)
450
- .HasForeignKey(d => d.WidgetId)
451
- .OnDelete(DeleteBehavior.Cascade)
452
- .HasConstraintName("FK_UserDashboardLayouts_DashboardWidgets");
453
- });
454
-
455
  modelBuilder.Entity<User>(entity =>
456
  {
457
  entity.HasIndex(e => e.RoleId, "IX_Users_RoleId");
 
24
 
25
  public virtual DbSet<Menu> Menus { get; set; }
26
 
 
 
27
  public virtual DbSet<Notification> Notifications { get; set; }
28
 
29
  public virtual DbSet<Permission> Permissions { get; set; }
 
36
 
37
  public virtual DbSet<RoleMenu> RoleMenus { get; set; }
38
 
 
 
39
  public virtual DbSet<RolePermission> RolePermissions { get; set; }
40
 
41
  public virtual DbSet<Task> Tasks { get; set; }
 
44
 
45
  public virtual DbSet<TimeLog> TimeLogs { get; set; }
46
 
 
 
47
  public virtual DbSet<UserDevice> UserDevices { get; set; }
48
 
49
  public virtual DbSet<User> Users { get; set; }
 
136
  .HasConstraintName("FK_Menus_ParentMenu");
137
  });
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  modelBuilder.Entity<Notification>(entity =>
140
  {
141
  entity.ToTable("notifications");
 
260
  .HasConstraintName("FK_RoleMenus_Roles");
261
  });
262
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  modelBuilder.Entity<RolePermission>(entity =>
264
  {
265
  entity.HasIndex(e => e.PermissionId, "IX_RolePermissions_PermissionId");
 
359
  .HasConstraintName("FK_TimeLogs_Users");
360
  });
361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  modelBuilder.Entity<User>(entity =>
363
  {
364
  entity.HasIndex(e => e.RoleId, "IX_Users_RoleId");
TaskTrackingSystem.Database/AppDbContextModels/DashboardWidget.cs DELETED
@@ -1,39 +0,0 @@
1
- using System;
2
- using System.Collections.Generic;
3
-
4
- namespace TaskTrackingSystem.Database.AppDbContextModels;
5
-
6
- public partial class DashboardWidget
7
- {
8
- public long WidgetId { get; set; }
9
-
10
- public string WidgetCode { get; set; } = null!;
11
-
12
- public string WidgetName { get; set; } = null!;
13
-
14
- public string? Description { get; set; }
15
-
16
- public string? Category { get; set; }
17
-
18
- public string? ComponentKey { get; set; }
19
-
20
- public string? DataSourceKey { get; set; }
21
-
22
- public int DefaultWidth { get; set; }
23
-
24
- public int DefaultHeight { get; set; }
25
-
26
- public int DefaultOrder { get; set; }
27
-
28
- public bool IsActive { get; set; }
29
-
30
- public bool IsDeleted { get; set; }
31
-
32
- public DateTime CreatedAt { get; set; }
33
-
34
- public DateTime? UpdatedAt { get; set; }
35
-
36
- public virtual ICollection<RoleDashboardWidget> RoleDashboardWidgets { get; set; } = new List<RoleDashboardWidget>();
37
-
38
- public virtual ICollection<UserDashboardLayout> UserDashboardLayouts { get; set; } = new List<UserDashboardLayout>();
39
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TaskTrackingSystem.Database/AppDbContextModels/Role.cs CHANGED
@@ -25,7 +25,5 @@ public partial class Role
25
 
26
  public virtual ICollection<RolePermission> RolePermissions { get; set; } = new List<RolePermission>();
27
 
28
- public virtual ICollection<RoleDashboardWidget> RoleDashboardWidgets { get; set; } = new List<RoleDashboardWidget>();
29
-
30
  public virtual ICollection<User> Users { get; set; } = new List<User>();
31
  }
 
25
 
26
  public virtual ICollection<RolePermission> RolePermissions { get; set; } = new List<RolePermission>();
27
 
 
 
28
  public virtual ICollection<User> Users { get; set; } = new List<User>();
29
  }
TaskTrackingSystem.Database/AppDbContextModels/RoleDashboardWidget.cs DELETED
@@ -1,42 +0,0 @@
1
- using System;
2
-
3
- namespace TaskTrackingSystem.Database.AppDbContextModels;
4
-
5
- public partial class RoleDashboardWidget
6
- {
7
- public long RoleDashboardWidgetId { get; set; }
8
-
9
- public long RoleId { get; set; }
10
-
11
- public long WidgetId { get; set; }
12
-
13
- public bool CanView { get; set; }
14
-
15
- public bool CanConfigure { get; set; }
16
-
17
- public bool IsDefaultVisible { get; set; }
18
-
19
- public int DefaultGridX { get; set; }
20
-
21
- public int DefaultGridY { get; set; }
22
-
23
- public int DefaultWidth { get; set; }
24
-
25
- public int DefaultHeight { get; set; }
26
-
27
- public int DefaultSortOrder { get; set; }
28
-
29
- public bool IsDeleted { get; set; }
30
-
31
- public long? CreatedById { get; set; }
32
-
33
- public DateTime CreatedAt { get; set; }
34
-
35
- public long? UpdatedById { get; set; }
36
-
37
- public DateTime? UpdatedAt { get; set; }
38
-
39
- public virtual DashboardWidget Widget { get; set; } = null!;
40
-
41
- public virtual Role Role { get; set; } = null!;
42
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TaskTrackingSystem.Database/AppDbContextModels/User.cs CHANGED
@@ -53,5 +53,4 @@ public partial class User
53
 
54
  public virtual ICollection<TimeLog> TimeLogs { get; set; } = new List<TimeLog>();
55
 
56
- public virtual ICollection<UserDashboardLayout> UserDashboardLayouts { get; set; } = new List<UserDashboardLayout>();
57
  }
 
53
 
54
  public virtual ICollection<TimeLog> TimeLogs { get; set; } = new List<TimeLog>();
55
 
 
56
  }
TaskTrackingSystem.Database/AppDbContextModels/UserDashboardLayout.cs DELETED
@@ -1,42 +0,0 @@
1
- using System;
2
-
3
- namespace TaskTrackingSystem.Database.AppDbContextModels;
4
-
5
- public partial class UserDashboardLayout
6
- {
7
- public long UserDashboardLayoutId { get; set; }
8
-
9
- public long UserId { get; set; }
10
-
11
- public long WidgetId { get; set; }
12
-
13
- public int GridX { get; set; }
14
-
15
- public int GridY { get; set; }
16
-
17
- public int Width { get; set; }
18
-
19
- public int Height { get; set; }
20
-
21
- public int SortOrder { get; set; }
22
-
23
- public bool IsHidden { get; set; }
24
-
25
- public bool IsPinned { get; set; }
26
-
27
- public string? ConfigJson { get; set; }
28
-
29
- public bool IsDeleted { get; set; }
30
-
31
- public long? CreatedById { get; set; }
32
-
33
- public DateTime CreatedAt { get; set; }
34
-
35
- public long? UpdatedById { get; set; }
36
-
37
- public DateTime? UpdatedAt { get; set; }
38
-
39
- public virtual DashboardWidget Widget { get; set; } = null!;
40
-
41
- public virtual User User { get; set; } = null!;
42
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetAdminDto.cs DELETED
@@ -1,37 +0,0 @@
1
- using System;
2
-
3
- namespace TaskTrackingSystem.Shared.Models.Dashboard
4
- {
5
- public class DashboardWidgetAdminDto
6
- {
7
- public long WidgetId { get; set; }
8
-
9
- public string WidgetCode { get; set; } = string.Empty;
10
-
11
- public string WidgetName { get; set; } = string.Empty;
12
-
13
- public string? Description { get; set; }
14
-
15
- public string? Category { get; set; }
16
-
17
- public string? ComponentKey { get; set; }
18
-
19
- public string? DataSourceKey { get; set; }
20
-
21
- public int DefaultWidth { get; set; }
22
-
23
- public int DefaultHeight { get; set; }
24
-
25
- public int DefaultOrder { get; set; }
26
-
27
- public bool IsActive { get; set; }
28
-
29
- public bool IsDeleted { get; set; }
30
-
31
- public DateTime CreatedAt { get; set; }
32
-
33
- public DateTime? UpdatedAt { get; set; }
34
-
35
- public int RoleCount { get; set; }
36
- }
37
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetDto.cs DELETED
@@ -1,49 +0,0 @@
1
- namespace TaskTrackingSystem.Shared.Models.Dashboard
2
- {
3
- public class DashboardWidgetDto
4
- {
5
- public long WidgetId { get; set; }
6
-
7
- public string WidgetCode { get; set; } = string.Empty;
8
-
9
- public string WidgetName { get; set; } = string.Empty;
10
-
11
- public string? Description { get; set; }
12
-
13
- public string? Category { get; set; }
14
-
15
- public string? ComponentKey { get; set; }
16
-
17
- public string? DataSourceKey { get; set; }
18
-
19
- public int DefaultWidth { get; set; }
20
-
21
- public int DefaultHeight { get; set; }
22
-
23
- public int DefaultOrder { get; set; }
24
-
25
- public bool CanView { get; set; }
26
-
27
- public bool CanConfigure { get; set; }
28
-
29
- public bool IsDefaultVisible { get; set; }
30
-
31
- public bool HasCustomLayout { get; set; }
32
-
33
- public bool IsHidden { get; set; }
34
-
35
- public bool IsPinned { get; set; }
36
-
37
- public int GridX { get; set; }
38
-
39
- public int GridY { get; set; }
40
-
41
- public int Width { get; set; }
42
-
43
- public int Height { get; set; }
44
-
45
- public int SortOrder { get; set; }
46
-
47
- public string? ConfigJson { get; set; }
48
- }
49
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetLayoutItemDto.cs DELETED
@@ -1,23 +0,0 @@
1
- namespace TaskTrackingSystem.Shared.Models.Dashboard
2
- {
3
- public class DashboardWidgetLayoutItemDto
4
- {
5
- public long WidgetId { get; set; }
6
-
7
- public int GridX { get; set; }
8
-
9
- public int GridY { get; set; }
10
-
11
- public int Width { get; set; }
12
-
13
- public int Height { get; set; }
14
-
15
- public int SortOrder { get; set; }
16
-
17
- public bool IsHidden { get; set; }
18
-
19
- public bool IsPinned { get; set; }
20
-
21
- public string? ConfigJson { get; set; }
22
- }
23
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetLayoutSaveRequestDto.cs DELETED
@@ -1,9 +0,0 @@
1
- using System.Collections.Generic;
2
-
3
- namespace TaskTrackingSystem.Shared.Models.Dashboard
4
- {
5
- public class DashboardWidgetLayoutSaveRequestDto
6
- {
7
- public List<DashboardWidgetLayoutItemDto> Widgets { get; set; } = new();
8
- }
9
- }
 
 
 
 
 
 
 
 
 
 
TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetRoleAccessDto.cs DELETED
@@ -1,25 +0,0 @@
1
- namespace TaskTrackingSystem.Shared.Models.Dashboard
2
- {
3
- public class DashboardWidgetRoleAccessDto
4
- {
5
- public long RoleId { get; set; }
6
-
7
- public string RoleName { get; set; } = string.Empty;
8
-
9
- public bool CanView { get; set; }
10
-
11
- public bool CanConfigure { get; set; }
12
-
13
- public bool IsDefaultVisible { get; set; }
14
-
15
- public int DefaultGridX { get; set; }
16
-
17
- public int DefaultGridY { get; set; }
18
-
19
- public int DefaultWidth { get; set; }
20
-
21
- public int DefaultHeight { get; set; }
22
-
23
- public int DefaultSortOrder { get; set; }
24
- }
25
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetRoleAccessSaveRequestDto.cs DELETED
@@ -1,9 +0,0 @@
1
- using System.Collections.Generic;
2
-
3
- namespace TaskTrackingSystem.Shared.Models.Dashboard
4
- {
5
- public class DashboardWidgetRoleAccessSaveRequestDto
6
- {
7
- public List<DashboardWidgetRoleAccessDto> RoleAccess { get; set; } = new();
8
- }
9
- }
 
 
 
 
 
 
 
 
 
 
TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetUpsertDto.cs DELETED
@@ -1,35 +0,0 @@
1
- using System.ComponentModel.DataAnnotations;
2
-
3
- namespace TaskTrackingSystem.Shared.Models.Dashboard
4
- {
5
- public class DashboardWidgetUpsertDto
6
- {
7
- public long WidgetId { get; set; }
8
-
9
- [Required, MaxLength(50)]
10
- public string WidgetCode { get; set; } = string.Empty;
11
-
12
- [Required, MaxLength(100)]
13
- public string WidgetName { get; set; } = string.Empty;
14
-
15
- [MaxLength(500)]
16
- public string? Description { get; set; }
17
-
18
- [MaxLength(50)]
19
- public string? Category { get; set; }
20
-
21
- [MaxLength(100)]
22
- public string? ComponentKey { get; set; }
23
-
24
- [MaxLength(100)]
25
- public string? DataSourceKey { get; set; }
26
-
27
- public int DefaultWidth { get; set; } = 4;
28
-
29
- public int DefaultHeight { get; set; } = 3;
30
-
31
- public int DefaultOrder { get; set; }
32
-
33
- public bool IsActive { get; set; } = true;
34
- }
35
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TaskTrackingSystem.WebApi/Features/Dashboard/DashboardController.cs CHANGED
@@ -1,5 +1,5 @@
1
- using Microsoft.AspNetCore.Mvc;
2
  using Microsoft.AspNetCore.Authorization;
 
3
  using System.Collections.Generic;
4
  using System.Threading.Tasks;
5
  using TaskTrackingSystem.Shared;
@@ -40,104 +40,5 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
40
  var result = await _dashboardService.GetProjectProgressAsync(User.GetRoleName(), User.GetUserId());
41
  return StatusCode(result.StatusCode, result);
42
  }
43
-
44
- [HttpGet("widgets")]
45
- public async Task<ActionResult<Result<IEnumerable<DashboardWidgetDto>>>> GetWidgets()
46
- {
47
- var result = await _dashboardService.GetWidgetsAsync(User.GetRoleName(), User.GetUserId());
48
- return StatusCode(result.StatusCode, result);
49
- }
50
-
51
- [HttpPut("widgets/layout")]
52
- public async Task<ActionResult<Result>> SaveWidgetLayout([FromBody] DashboardWidgetLayoutSaveRequestDto request)
53
- {
54
- var result = await _dashboardService.SaveWidgetLayoutAsync(User.GetRoleName(), User.GetUserId(), request);
55
- return StatusCode(result.StatusCode, result);
56
- }
57
-
58
- [HttpGet("admin/widgets")]
59
- public async Task<ActionResult<Result<IEnumerable<DashboardWidgetAdminDto>>>> GetWidgetCatalog()
60
- {
61
- if (!User.IsInRole("Admin"))
62
- {
63
- return Forbid();
64
- }
65
-
66
- var result = await _dashboardService.GetWidgetCatalogAsync();
67
- return StatusCode(result.StatusCode, result);
68
- }
69
-
70
- [HttpGet("admin/widgets/{widgetId:long}")]
71
- public async Task<ActionResult<Result<DashboardWidgetAdminDto>>> GetWidget(long widgetId)
72
- {
73
- if (!User.IsInRole("Admin"))
74
- {
75
- return Forbid();
76
- }
77
-
78
- var result = await _dashboardService.GetWidgetByIdAsync(widgetId);
79
- return StatusCode(result.StatusCode, result);
80
- }
81
-
82
- [HttpPost("admin/widgets")]
83
- public async Task<ActionResult<Result<DashboardWidgetAdminDto>>> CreateWidget([FromBody] DashboardWidgetUpsertDto request)
84
- {
85
- if (!User.IsInRole("Admin"))
86
- {
87
- return Forbid();
88
- }
89
-
90
- var result = await _dashboardService.SaveWidgetAsync(request, User.GetUserId());
91
- return StatusCode(result.StatusCode, result);
92
- }
93
-
94
- [HttpPut("admin/widgets/{widgetId:long}")]
95
- public async Task<ActionResult<Result<DashboardWidgetAdminDto>>> UpdateWidget(long widgetId, [FromBody] DashboardWidgetUpsertDto request)
96
- {
97
- if (!User.IsInRole("Admin"))
98
- {
99
- return Forbid();
100
- }
101
-
102
- request.WidgetId = widgetId;
103
- var result = await _dashboardService.SaveWidgetAsync(request, User.GetUserId());
104
- return StatusCode(result.StatusCode, result);
105
- }
106
-
107
- [HttpDelete("admin/widgets/{widgetId:long}")]
108
- public async Task<ActionResult<Result>> DeleteWidget(long widgetId)
109
- {
110
- if (!User.IsInRole("Admin"))
111
- {
112
- return Forbid();
113
- }
114
-
115
- var result = await _dashboardService.DeleteWidgetAsync(widgetId, User.GetUserId());
116
- return StatusCode(result.StatusCode, result);
117
- }
118
-
119
- [HttpGet("admin/widgets/{widgetId:long}/access")]
120
- public async Task<ActionResult<Result<IEnumerable<DashboardWidgetRoleAccessDto>>>> GetWidgetAccess(long widgetId)
121
- {
122
- if (!User.IsInRole("Admin"))
123
- {
124
- return Forbid();
125
- }
126
-
127
- var result = await _dashboardService.GetWidgetAccessAsync(widgetId);
128
- return StatusCode(result.StatusCode, result);
129
- }
130
-
131
- [HttpPut("admin/widgets/{widgetId:long}/access")]
132
- public async Task<ActionResult<Result>> SaveWidgetAccess(long widgetId, [FromBody] DashboardWidgetRoleAccessSaveRequestDto request)
133
- {
134
- if (!User.IsInRole("Admin"))
135
- {
136
- return Forbid();
137
- }
138
-
139
- var result = await _dashboardService.SaveWidgetAccessAsync(widgetId, request, User.GetUserId());
140
- return StatusCode(result.StatusCode, result);
141
- }
142
  }
143
  }
 
 
1
  using Microsoft.AspNetCore.Authorization;
2
+ using Microsoft.AspNetCore.Mvc;
3
  using System.Collections.Generic;
4
  using System.Threading.Tasks;
5
  using TaskTrackingSystem.Shared;
 
40
  var result = await _dashboardService.GetProjectProgressAsync(User.GetRoleName(), User.GetUserId());
41
  return StatusCode(result.StatusCode, result);
42
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
  }
TaskTrackingSystem.WebApi/Features/Dashboard/DashboardService.cs CHANGED
@@ -5,9 +5,8 @@ using System.Linq;
5
  using System.Threading.Tasks;
6
  using TaskTrackingSystem.Database.AppDbContextModels;
7
  using TaskTrackingSystem.Shared;
8
- using TaskTrackingSystem.Shared.Models.Dashboard;
9
  using TaskTrackingSystem.Shared.Enums;
10
- using RoleEntity = TaskTrackingSystem.Database.AppDbContextModels.Role;
11
 
12
  namespace TaskTrackingSystem.WebApi.Features.Dashboard
13
  {
@@ -58,7 +57,6 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
58
  })
59
  .ToListAsync();
60
 
61
- // Status mappings (StatusId 1 = To Do, 2 = In Progress, 3 = Done etc.)
62
  var statusMap = new Dictionary<AppTaskStatus, string>
63
  {
64
  { AppTaskStatus.Todo, "To Do" },
@@ -73,7 +71,6 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
73
  TaskCount = gt.Count
74
  }).ToList();
75
 
76
- // Make sure standard statuses are represented even if count is 0
77
  foreach (var status in statusMap)
78
  {
79
  if (!overview.Any(o => o.StatusId == status.Key))
@@ -92,9 +89,7 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
92
 
93
  public async Task<Result<IEnumerable<ProjectProgressDto>>> GetProjectProgressAsync(string roleName, long currentUserId)
94
  {
95
- var activeProjects = await BuildAccessibleProjectQuery(roleName, currentUserId)
96
- .ToListAsync();
97
-
98
  var progressList = new List<ProjectProgressDto>();
99
 
100
  foreach (var project in activeProjects)
@@ -104,8 +99,7 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
104
  .ToListAsync();
105
 
106
  int totalTasks = tasks.Count;
107
- int completedTasks = tasks.Count(t => t.StatusId == AppTaskStatus.Done); // StatusId 3 = Done
108
-
109
  double percentage = totalTasks > 0 ? Math.Round(((double)completedTasks / totalTasks) * 100, 2) : 0;
110
 
111
  progressList.Add(new ProjectProgressDto
@@ -121,515 +115,6 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
121
  return Result<IEnumerable<ProjectProgressDto>>.Success(progressList);
122
  }
123
 
124
- public async Task<Result<IEnumerable<DashboardWidgetDto>>> GetWidgetsAsync(string roleName, long currentUserId)
125
- {
126
- var role = await GetRoleAsync(roleName);
127
- if (role == null)
128
- {
129
- return Result<IEnumerable<DashboardWidgetDto>>.Failure("Role not found.", 404);
130
- }
131
-
132
- var roleWidgets = await (
133
- from access in _db.RoleDashboardWidgets
134
- join widget in _db.DashboardWidgets on access.WidgetId equals widget.WidgetId
135
- where access.RoleId == role.Id &&
136
- !access.IsDeleted &&
137
- access.CanView &&
138
- !widget.IsDeleted &&
139
- widget.IsActive
140
- orderby access.DefaultSortOrder, widget.DefaultOrder, widget.WidgetName
141
- select new
142
- {
143
- access.WidgetId,
144
- access.CanView,
145
- access.CanConfigure,
146
- access.IsDefaultVisible,
147
- RoleDefaultGridX = access.DefaultGridX,
148
- RoleDefaultGridY = access.DefaultGridY,
149
- RoleDefaultWidth = access.DefaultWidth,
150
- RoleDefaultHeight = access.DefaultHeight,
151
- RoleDefaultSortOrder = access.DefaultSortOrder,
152
- widget.WidgetCode,
153
- widget.WidgetName,
154
- widget.Description,
155
- widget.Category,
156
- widget.ComponentKey,
157
- widget.DataSourceKey,
158
- WidgetDefaultWidth = widget.DefaultWidth,
159
- WidgetDefaultHeight = widget.DefaultHeight,
160
- WidgetDefaultOrder = widget.DefaultOrder
161
- }).ToListAsync();
162
-
163
- var userLayouts = await _db.UserDashboardLayouts
164
- .Where(layout => layout.UserId == currentUserId && !layout.IsDeleted)
165
- .ToListAsync();
166
-
167
- var layoutLookup = userLayouts.ToDictionary(layout => layout.WidgetId);
168
-
169
- var widgets = roleWidgets
170
- .Select(widget =>
171
- {
172
- layoutLookup.TryGetValue(widget.WidgetId, out var layout);
173
-
174
- var width = layout != null ? layout.Width : (widget.RoleDefaultWidth > 0 ? widget.RoleDefaultWidth : widget.WidgetDefaultWidth);
175
- var height = layout != null ? layout.Height : (widget.RoleDefaultHeight > 0 ? widget.RoleDefaultHeight : widget.WidgetDefaultHeight);
176
- var sortOrder = layout != null ? layout.SortOrder : widget.RoleDefaultSortOrder;
177
-
178
- return new DashboardWidgetDto
179
- {
180
- WidgetId = widget.WidgetId,
181
- WidgetCode = widget.WidgetCode,
182
- WidgetName = widget.WidgetName,
183
- Description = widget.Description,
184
- Category = widget.Category,
185
- ComponentKey = widget.ComponentKey,
186
- DataSourceKey = widget.DataSourceKey,
187
- DefaultWidth = widget.WidgetDefaultWidth,
188
- DefaultHeight = widget.WidgetDefaultHeight,
189
- DefaultOrder = widget.WidgetDefaultOrder,
190
- CanView = widget.CanView,
191
- CanConfigure = widget.CanConfigure,
192
- IsDefaultVisible = widget.IsDefaultVisible,
193
- HasCustomLayout = layout != null,
194
- IsHidden = layout?.IsHidden ?? !widget.IsDefaultVisible,
195
- IsPinned = layout?.IsPinned ?? false,
196
- GridX = layout?.GridX ?? widget.RoleDefaultGridX,
197
- GridY = layout?.GridY ?? widget.RoleDefaultGridY,
198
- Width = width,
199
- Height = height,
200
- SortOrder = sortOrder,
201
- ConfigJson = layout?.ConfigJson
202
- };
203
- })
204
- .OrderBy(widget => widget.SortOrder)
205
- .ThenBy(widget => widget.GridY)
206
- .ThenBy(widget => widget.GridX)
207
- .ThenBy(widget => widget.DefaultOrder)
208
- .ToList();
209
-
210
- return Result<IEnumerable<DashboardWidgetDto>>.Success(widgets);
211
- }
212
-
213
- public async Task<Result> SaveWidgetLayoutAsync(string roleName, long currentUserId, DashboardWidgetLayoutSaveRequestDto request)
214
- {
215
- var role = await GetRoleAsync(roleName);
216
- if (role == null)
217
- {
218
- return Result.Failure("Role not found.", 404);
219
- }
220
-
221
- if (request == null)
222
- {
223
- return Result.Failure("Widget layout is required.", 400);
224
- }
225
-
226
- if (request.Widgets == null)
227
- {
228
- return Result.Failure("Widget layout is required.", 400);
229
- }
230
-
231
- var allowedWidgetIds = await _db.RoleDashboardWidgets
232
- .Where(access => access.RoleId == role.Id && !access.IsDeleted && access.CanView)
233
- .Select(access => access.WidgetId)
234
- .ToHashSetAsync();
235
-
236
- var requestedWidgets = request.Widgets
237
- .GroupBy(widget => widget.WidgetId)
238
- .Select(group => group.Last())
239
- .ToList();
240
-
241
- var invalidWidgets = requestedWidgets
242
- .Where(widget => !allowedWidgetIds.Contains(widget.WidgetId))
243
- .Select(widget => widget.WidgetId)
244
- .Distinct()
245
- .ToList();
246
-
247
- if (invalidWidgets.Any())
248
- {
249
- return Result.Failure($"The following widgets are not available for this role: {string.Join(", ", invalidWidgets)}", 403);
250
- }
251
-
252
- var existingLayouts = await _db.UserDashboardLayouts
253
- .Where(layout => layout.UserId == currentUserId && !layout.IsDeleted)
254
- .ToListAsync();
255
- var existingLookup = existingLayouts.ToDictionary(layout => layout.WidgetId);
256
-
257
- var now = DateTime.UtcNow;
258
- foreach (var widget in requestedWidgets)
259
- {
260
- if (existingLookup.TryGetValue(widget.WidgetId, out var existing))
261
- {
262
- existing.GridX = widget.GridX;
263
- existing.GridY = widget.GridY;
264
- existing.Width = widget.Width > 0 ? widget.Width : existing.Width;
265
- existing.Height = widget.Height > 0 ? widget.Height : existing.Height;
266
- existing.SortOrder = widget.SortOrder;
267
- existing.IsHidden = widget.IsHidden;
268
- existing.IsPinned = widget.IsPinned;
269
- existing.ConfigJson = widget.ConfigJson;
270
- existing.IsDeleted = false;
271
- existing.UpdatedAt = now;
272
- existing.UpdatedById = currentUserId;
273
- }
274
- else
275
- {
276
- _db.UserDashboardLayouts.Add(new UserDashboardLayout
277
- {
278
- UserId = currentUserId,
279
- WidgetId = widget.WidgetId,
280
- GridX = widget.GridX,
281
- GridY = widget.GridY,
282
- Width = widget.Width > 0 ? widget.Width : 4,
283
- Height = widget.Height > 0 ? widget.Height : 3,
284
- SortOrder = widget.SortOrder,
285
- IsHidden = widget.IsHidden,
286
- IsPinned = widget.IsPinned,
287
- ConfigJson = widget.ConfigJson,
288
- IsDeleted = false,
289
- CreatedAt = now,
290
- UpdatedAt = now,
291
- CreatedById = currentUserId,
292
- UpdatedById = currentUserId
293
- });
294
- }
295
- }
296
-
297
- await _db.SaveChangesAsync();
298
- return Result.Success(200);
299
- }
300
-
301
- public async Task<Result<IEnumerable<DashboardWidgetAdminDto>>> GetWidgetCatalogAsync()
302
- {
303
- var roleCounts = await _db.RoleDashboardWidgets
304
- .Where(access => !access.IsDeleted)
305
- .GroupBy(access => access.WidgetId)
306
- .Select(group => new
307
- {
308
- WidgetId = group.Key,
309
- RoleCount = group.Select(item => item.RoleId).Distinct().Count()
310
- })
311
- .ToListAsync();
312
-
313
- var roleCountLookup = roleCounts.ToDictionary(x => x.WidgetId, x => x.RoleCount);
314
-
315
- var widgets = await _db.DashboardWidgets
316
- .OrderBy(widget => widget.DefaultOrder)
317
- .ThenBy(widget => widget.WidgetName)
318
- .Select(widget => new DashboardWidgetAdminDto
319
- {
320
- WidgetId = widget.WidgetId,
321
- WidgetCode = widget.WidgetCode,
322
- WidgetName = widget.WidgetName,
323
- Description = widget.Description,
324
- Category = widget.Category,
325
- ComponentKey = widget.ComponentKey,
326
- DataSourceKey = widget.DataSourceKey,
327
- DefaultWidth = widget.DefaultWidth,
328
- DefaultHeight = widget.DefaultHeight,
329
- DefaultOrder = widget.DefaultOrder,
330
- IsActive = widget.IsActive,
331
- IsDeleted = widget.IsDeleted,
332
- CreatedAt = widget.CreatedAt,
333
- UpdatedAt = widget.UpdatedAt,
334
- RoleCount = 0
335
- })
336
- .ToListAsync();
337
-
338
- foreach (var widget in widgets)
339
- {
340
- if (roleCountLookup.TryGetValue(widget.WidgetId, out var roleCount))
341
- {
342
- widget.RoleCount = roleCount;
343
- }
344
- }
345
-
346
- return Result<IEnumerable<DashboardWidgetAdminDto>>.Success(widgets);
347
- }
348
-
349
- public async Task<Result<DashboardWidgetAdminDto>> GetWidgetByIdAsync(long widgetId)
350
- {
351
- var widget = await _db.DashboardWidgets.FirstOrDefaultAsync(w => w.WidgetId == widgetId);
352
- if (widget == null)
353
- {
354
- return Result<DashboardWidgetAdminDto>.Failure("Widget not found.", 404);
355
- }
356
-
357
- var roleCount = await _db.RoleDashboardWidgets
358
- .Where(access => access.WidgetId == widgetId && !access.IsDeleted)
359
- .Select(access => access.RoleId)
360
- .Distinct()
361
- .CountAsync();
362
-
363
- return Result<DashboardWidgetAdminDto>.Success(new DashboardWidgetAdminDto
364
- {
365
- WidgetId = widget.WidgetId,
366
- WidgetCode = widget.WidgetCode,
367
- WidgetName = widget.WidgetName,
368
- Description = widget.Description,
369
- Category = widget.Category,
370
- ComponentKey = widget.ComponentKey,
371
- DataSourceKey = widget.DataSourceKey,
372
- DefaultWidth = widget.DefaultWidth,
373
- DefaultHeight = widget.DefaultHeight,
374
- DefaultOrder = widget.DefaultOrder,
375
- IsActive = widget.IsActive,
376
- IsDeleted = widget.IsDeleted,
377
- CreatedAt = widget.CreatedAt,
378
- UpdatedAt = widget.UpdatedAt,
379
- RoleCount = roleCount
380
- });
381
- }
382
-
383
- public async Task<Result<DashboardWidgetAdminDto>> SaveWidgetAsync(DashboardWidgetUpsertDto dto, long? currentUserId = null)
384
- {
385
- if (dto == null)
386
- {
387
- return Result<DashboardWidgetAdminDto>.Failure("Widget data is required.", 400);
388
- }
389
-
390
- if (string.IsNullOrWhiteSpace(dto.WidgetCode))
391
- {
392
- return Result<DashboardWidgetAdminDto>.Failure("Widget code is required.", 400);
393
- }
394
-
395
- if (string.IsNullOrWhiteSpace(dto.WidgetName))
396
- {
397
- return Result<DashboardWidgetAdminDto>.Failure("Widget name is required.", 400);
398
- }
399
-
400
- var widgetCode = dto.WidgetCode.Trim();
401
- var widgetName = dto.WidgetName.Trim();
402
-
403
- var existingByCode = await _db.DashboardWidgets
404
- .FirstOrDefaultAsync(widget => widget.WidgetCode == widgetCode && widget.WidgetId != dto.WidgetId);
405
- if (existingByCode != null)
406
- {
407
- return Result<DashboardWidgetAdminDto>.Failure("Widget code is already in use by another widget.", 400);
408
- }
409
-
410
- var now = DateTime.UtcNow;
411
- DashboardWidget? widget = null;
412
-
413
- if (dto.WidgetId > 0)
414
- {
415
- widget = await _db.DashboardWidgets.FirstOrDefaultAsync(item => item.WidgetId == dto.WidgetId);
416
- if (widget == null)
417
- {
418
- return Result<DashboardWidgetAdminDto>.Failure("Widget not found.", 404);
419
- }
420
- }
421
- else
422
- {
423
- widget = new DashboardWidget
424
- {
425
- CreatedAt = now
426
- };
427
- _db.DashboardWidgets.Add(widget);
428
- }
429
-
430
- var activeWidget = widget!;
431
-
432
- activeWidget.WidgetCode = widgetCode;
433
- activeWidget.WidgetName = widgetName;
434
- activeWidget.Description = string.IsNullOrWhiteSpace(dto.Description) ? null : dto.Description.Trim();
435
- activeWidget.Category = string.IsNullOrWhiteSpace(dto.Category) ? null : dto.Category.Trim();
436
- activeWidget.ComponentKey = string.IsNullOrWhiteSpace(dto.ComponentKey) ? null : dto.ComponentKey.Trim();
437
- activeWidget.DataSourceKey = string.IsNullOrWhiteSpace(dto.DataSourceKey) ? null : dto.DataSourceKey.Trim();
438
- activeWidget.DefaultWidth = dto.DefaultWidth > 0 ? dto.DefaultWidth : 4;
439
- activeWidget.DefaultHeight = dto.DefaultHeight > 0 ? dto.DefaultHeight : 3;
440
- activeWidget.DefaultOrder = dto.DefaultOrder;
441
- activeWidget.IsActive = dto.IsActive;
442
- activeWidget.IsDeleted = false;
443
- activeWidget.UpdatedAt = now;
444
-
445
- if (dto.WidgetId > 0)
446
- {
447
- _db.DashboardWidgets.Update(activeWidget);
448
- }
449
-
450
- await _db.SaveChangesAsync();
451
-
452
- var roleCount = await _db.RoleDashboardWidgets
453
- .Where(access => access.WidgetId == activeWidget.WidgetId && !access.IsDeleted)
454
- .Select(access => access.RoleId)
455
- .Distinct()
456
- .CountAsync();
457
-
458
- return Result<DashboardWidgetAdminDto>.Success(new DashboardWidgetAdminDto
459
- {
460
- WidgetId = activeWidget.WidgetId,
461
- WidgetCode = activeWidget.WidgetCode,
462
- WidgetName = activeWidget.WidgetName,
463
- Description = activeWidget.Description,
464
- Category = activeWidget.Category,
465
- ComponentKey = activeWidget.ComponentKey,
466
- DataSourceKey = activeWidget.DataSourceKey,
467
- DefaultWidth = activeWidget.DefaultWidth,
468
- DefaultHeight = activeWidget.DefaultHeight,
469
- DefaultOrder = activeWidget.DefaultOrder,
470
- IsActive = activeWidget.IsActive,
471
- IsDeleted = activeWidget.IsDeleted,
472
- CreatedAt = activeWidget.CreatedAt,
473
- UpdatedAt = activeWidget.UpdatedAt,
474
- RoleCount = roleCount
475
- }, dto.WidgetId > 0 ? 200 : 201);
476
- }
477
-
478
- public async Task<Result> DeleteWidgetAsync(long widgetId, long? currentUserId = null)
479
- {
480
- var widget = await _db.DashboardWidgets.FirstOrDefaultAsync(item => item.WidgetId == widgetId);
481
- if (widget == null)
482
- {
483
- return Result.Failure("Widget not found.", 404);
484
- }
485
-
486
- var now = DateTime.UtcNow;
487
- widget.IsDeleted = true;
488
- widget.IsActive = false;
489
- widget.UpdatedAt = now;
490
-
491
- var roleAccessRows = await _db.RoleDashboardWidgets
492
- .Where(access => access.WidgetId == widgetId && !access.IsDeleted)
493
- .ToListAsync();
494
- foreach (var row in roleAccessRows)
495
- {
496
- row.IsDeleted = true;
497
- row.UpdatedAt = now;
498
- row.UpdatedById = currentUserId;
499
- }
500
-
501
- var userLayoutRows = await _db.UserDashboardLayouts
502
- .Where(layout => layout.WidgetId == widgetId && !layout.IsDeleted)
503
- .ToListAsync();
504
- foreach (var row in userLayoutRows)
505
- {
506
- row.IsDeleted = true;
507
- row.UpdatedAt = now;
508
- row.UpdatedById = currentUserId;
509
- }
510
-
511
- _db.DashboardWidgets.Update(widget);
512
- await _db.SaveChangesAsync();
513
- return Result.Success(200);
514
- }
515
-
516
- public async Task<Result<IEnumerable<DashboardWidgetRoleAccessDto>>> GetWidgetAccessAsync(long widgetId)
517
- {
518
- var widgetExists = await _db.DashboardWidgets.AnyAsync(widget => widget.WidgetId == widgetId && !widget.IsDeleted);
519
- if (!widgetExists)
520
- {
521
- return Result<IEnumerable<DashboardWidgetRoleAccessDto>>.Failure("Widget not found.", 404);
522
- }
523
-
524
- var roles = await _db.Roles
525
- .Where(role => !role.IsDeleted)
526
- .OrderBy(role => role.Name)
527
- .ToListAsync();
528
-
529
- var accessRows = await _db.RoleDashboardWidgets
530
- .Where(access => access.WidgetId == widgetId && !access.IsDeleted)
531
- .ToListAsync();
532
- var lookup = accessRows.ToDictionary(row => row.RoleId);
533
-
534
- var access = roles.Select(role =>
535
- {
536
- lookup.TryGetValue(role.Id, out var row);
537
- return new DashboardWidgetRoleAccessDto
538
- {
539
- RoleId = role.Id,
540
- RoleName = role.Name,
541
- CanView = row?.CanView ?? false,
542
- CanConfigure = row?.CanConfigure ?? false,
543
- IsDefaultVisible = row?.IsDefaultVisible ?? false,
544
- DefaultGridX = row?.DefaultGridX ?? 0,
545
- DefaultGridY = row?.DefaultGridY ?? 0,
546
- DefaultWidth = row?.DefaultWidth ?? 4,
547
- DefaultHeight = row?.DefaultHeight ?? 3,
548
- DefaultSortOrder = row?.DefaultSortOrder ?? 0
549
- };
550
- }).ToList();
551
-
552
- return Result<IEnumerable<DashboardWidgetRoleAccessDto>>.Success(access);
553
- }
554
-
555
- public async Task<Result> SaveWidgetAccessAsync(long widgetId, DashboardWidgetRoleAccessSaveRequestDto request, long? currentUserId = null)
556
- {
557
- var widget = await _db.DashboardWidgets.FirstOrDefaultAsync(item => item.WidgetId == widgetId && !item.IsDeleted);
558
- if (widget == null)
559
- {
560
- return Result.Failure("Widget not found.", 404);
561
- }
562
-
563
- if (request?.RoleAccess == null)
564
- {
565
- return Result.Failure("Role access data is required.", 400);
566
- }
567
-
568
- var roles = await _db.Roles
569
- .Where(role => !role.IsDeleted)
570
- .Select(role => role.Id)
571
- .ToHashSetAsync();
572
-
573
- var roleAccess = request.RoleAccess
574
- .Where(item => roles.Contains(item.RoleId))
575
- .GroupBy(item => item.RoleId)
576
- .Select(group => group.Last())
577
- .ToList();
578
-
579
- if (roleAccess.Count == 0)
580
- {
581
- return Result.Failure("No valid role access rows were supplied.", 400);
582
- }
583
-
584
- var existingRows = await _db.RoleDashboardWidgets
585
- .Where(access => access.WidgetId == widgetId)
586
- .ToListAsync();
587
- var lookup = existingRows.ToDictionary(row => row.RoleId);
588
- var now = DateTime.UtcNow;
589
-
590
- foreach (var item in roleAccess)
591
- {
592
- if (lookup.TryGetValue(item.RoleId, out var existing))
593
- {
594
- existing.CanView = item.CanView;
595
- existing.CanConfigure = item.CanConfigure;
596
- existing.IsDefaultVisible = item.IsDefaultVisible;
597
- existing.DefaultGridX = item.DefaultGridX;
598
- existing.DefaultGridY = item.DefaultGridY;
599
- existing.DefaultWidth = item.DefaultWidth > 0 ? item.DefaultWidth : 4;
600
- existing.DefaultHeight = item.DefaultHeight > 0 ? item.DefaultHeight : 3;
601
- existing.DefaultSortOrder = item.DefaultSortOrder;
602
- existing.IsDeleted = false;
603
- existing.UpdatedAt = now;
604
- existing.UpdatedById = currentUserId;
605
- }
606
- else
607
- {
608
- _db.RoleDashboardWidgets.Add(new RoleDashboardWidget
609
- {
610
- RoleId = item.RoleId,
611
- WidgetId = widgetId,
612
- CanView = item.CanView,
613
- CanConfigure = item.CanConfigure,
614
- IsDefaultVisible = item.IsDefaultVisible,
615
- DefaultGridX = item.DefaultGridX,
616
- DefaultGridY = item.DefaultGridY,
617
- DefaultWidth = item.DefaultWidth > 0 ? item.DefaultWidth : 4,
618
- DefaultHeight = item.DefaultHeight > 0 ? item.DefaultHeight : 3,
619
- DefaultSortOrder = item.DefaultSortOrder,
620
- IsDeleted = false,
621
- CreatedAt = now,
622
- UpdatedAt = now,
623
- CreatedById = currentUserId,
624
- UpdatedById = currentUserId
625
- });
626
- }
627
- }
628
-
629
- await _db.SaveChangesAsync();
630
- return Result.Success(200);
631
- }
632
-
633
  private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Task> BuildAccessibleTaskQuery(string roleName, long currentUserId)
634
  {
635
  var query = _db.Tasks.Where(t => t.IsDeleted != true);
@@ -671,16 +156,6 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
671
  return string.Equals(roleName, "Admin", StringComparison.OrdinalIgnoreCase);
672
  }
673
 
674
- private async Task<RoleEntity?> GetRoleAsync(string roleName)
675
- {
676
- if (string.IsNullOrWhiteSpace(roleName))
677
- {
678
- return null;
679
- }
680
-
681
- return await _db.Roles.FirstOrDefaultAsync(role => role.Name == roleName && !role.IsDeleted);
682
- }
683
-
684
  private static bool IsManager(string roleName)
685
  {
686
  return string.Equals(roleName, "Manager", StringComparison.OrdinalIgnoreCase);
 
5
  using System.Threading.Tasks;
6
  using TaskTrackingSystem.Database.AppDbContextModels;
7
  using TaskTrackingSystem.Shared;
 
8
  using TaskTrackingSystem.Shared.Enums;
9
+ using TaskTrackingSystem.Shared.Models.Dashboard;
10
 
11
  namespace TaskTrackingSystem.WebApi.Features.Dashboard
12
  {
 
57
  })
58
  .ToListAsync();
59
 
 
60
  var statusMap = new Dictionary<AppTaskStatus, string>
61
  {
62
  { AppTaskStatus.Todo, "To Do" },
 
71
  TaskCount = gt.Count
72
  }).ToList();
73
 
 
74
  foreach (var status in statusMap)
75
  {
76
  if (!overview.Any(o => o.StatusId == status.Key))
 
89
 
90
  public async Task<Result<IEnumerable<ProjectProgressDto>>> GetProjectProgressAsync(string roleName, long currentUserId)
91
  {
92
+ var activeProjects = await BuildAccessibleProjectQuery(roleName, currentUserId).ToListAsync();
 
 
93
  var progressList = new List<ProjectProgressDto>();
94
 
95
  foreach (var project in activeProjects)
 
99
  .ToListAsync();
100
 
101
  int totalTasks = tasks.Count;
102
+ int completedTasks = tasks.Count(t => t.StatusId == AppTaskStatus.Done);
 
103
  double percentage = totalTasks > 0 ? Math.Round(((double)completedTasks / totalTasks) * 100, 2) : 0;
104
 
105
  progressList.Add(new ProjectProgressDto
 
115
  return Result<IEnumerable<ProjectProgressDto>>.Success(progressList);
116
  }
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Task> BuildAccessibleTaskQuery(string roleName, long currentUserId)
119
  {
120
  var query = _db.Tasks.Where(t => t.IsDeleted != true);
 
156
  return string.Equals(roleName, "Admin", StringComparison.OrdinalIgnoreCase);
157
  }
158
 
 
 
 
 
 
 
 
 
 
 
159
  private static bool IsManager(string roleName)
160
  {
161
  return string.Equals(roleName, "Manager", StringComparison.OrdinalIgnoreCase);
TaskTrackingSystem.WebApi/Program.cs CHANGED
@@ -128,147 +128,6 @@ static async System.Threading.Tasks.Task EnsureSeedDataAsync(WebApplication app)
128
  backlogMenu.UpdatedAt = DateTime.UtcNow;
129
  }
130
 
131
- var widgetSetupMenu = await db.Menus.FirstOrDefaultAsync(m => m.MenuCode == "DASHBOARD_WIDGETS");
132
- if (widgetSetupMenu == null)
133
- {
134
- widgetSetupMenu = new Menu
135
- {
136
- MenuCode = "DASHBOARD_WIDGETS",
137
- MenuName = "Widget Setup",
138
- MenuUrl = "/widget-setup",
139
- Icon = "layout-grid",
140
- Visible = true,
141
- OrderNo = 26,
142
- IsDeleted = false,
143
- CreatedAt = DateTime.UtcNow
144
- };
145
- db.Menus.Add(widgetSetupMenu);
146
- await db.SaveChangesAsync();
147
- }
148
- else
149
- {
150
- widgetSetupMenu.MenuName = "Widget Setup";
151
- widgetSetupMenu.MenuUrl = "/widget-setup";
152
- widgetSetupMenu.Icon = "layout-grid";
153
- widgetSetupMenu.Visible = true;
154
- widgetSetupMenu.OrderNo = 26;
155
- widgetSetupMenu.IsDeleted = false;
156
- widgetSetupMenu.UpdatedAt = DateTime.UtcNow;
157
- }
158
-
159
- var adminRole = await db.Roles.FirstOrDefaultAsync(r => r.Name == "Admin" && !r.IsDeleted);
160
- if (adminRole != null && widgetSetupMenu.MenuId > 0)
161
- {
162
- var adminMenuAccess = await db.RoleMenus.FirstOrDefaultAsync(rm => rm.RoleId == adminRole.Id && rm.MenuId == widgetSetupMenu.MenuId);
163
- if (adminMenuAccess == null)
164
- {
165
- db.RoleMenus.Add(new RoleMenu
166
- {
167
- RoleId = adminRole.Id,
168
- MenuId = widgetSetupMenu.MenuId,
169
- IsDeleted = false,
170
- CreatedAt = DateTime.UtcNow
171
- });
172
- }
173
- else
174
- {
175
- adminMenuAccess.IsDeleted = false;
176
- adminMenuAccess.UpdatedAt = DateTime.UtcNow;
177
- }
178
- }
179
-
180
- var widgetSeeds = new[]
181
- {
182
- new DashboardWidget { WidgetCode = "WIDGET_GREETING_BANNER", WidgetName = "Greeting Banner", Description = "A friendly greeting panel with the current date.", Category = "Overview", ComponentKey = "GreetingBanner", DataSourceKey = "CurrentUser", DefaultWidth = 4, DefaultHeight = 2, DefaultOrder = 1, IsActive = true, IsDeleted = false, CreatedAt = DateTime.UtcNow },
183
- new DashboardWidget { WidgetCode = "WIDGET_KPI_CARDS", WidgetName = "KPI Cards", Description = "Quick counts for users, projects, and tasks.", Category = "Overview", ComponentKey = "KpiCards", DataSourceKey = "DashboardSummary", DefaultWidth = 4, DefaultHeight = 2, DefaultOrder = 2, IsActive = true, IsDeleted = false, CreatedAt = DateTime.UtcNow },
184
- new DashboardWidget { WidgetCode = "WIDGET_CALENDAR_STRIP", WidgetName = "Calendar Strip", Description = "A compact weekly view with due tasks.", Category = "Planning", ComponentKey = "CalendarStrip", DataSourceKey = "DueTasks", DefaultWidth = 4, DefaultHeight = 3, DefaultOrder = 3, IsActive = true, IsDeleted = false, CreatedAt = DateTime.UtcNow },
185
- new DashboardWidget { WidgetCode = "WIDGET_PROJECT_PROGRESS", WidgetName = "Project Progress", Description = "Shows completion progress for active projects.", Category = "Analytics", ComponentKey = "ProjectProgressChart", DataSourceKey = "ProjectProgress", DefaultWidth = 4, DefaultHeight = 3, DefaultOrder = 4, IsActive = true, IsDeleted = false, CreatedAt = DateTime.UtcNow },
186
- new DashboardWidget { WidgetCode = "WIDGET_TIMELINE", WidgetName = "Timeline", Description = "Highlights project timing and start/end dates.", Category = "Planning", ComponentKey = "TimelineChart", DataSourceKey = "Projects", DefaultWidth = 4, DefaultHeight = 3, DefaultOrder = 5, IsActive = true, IsDeleted = false, CreatedAt = DateTime.UtcNow },
187
- new DashboardWidget { WidgetCode = "WIDGET_TASK_TRAY", WidgetName = "Task Tray", Description = "A small queue of work the user can pick up next.", Category = "Work", ComponentKey = "TaskTray", DataSourceKey = "Tasks", DefaultWidth = 4, DefaultHeight = 3, DefaultOrder = 6, IsActive = true, IsDeleted = false, CreatedAt = DateTime.UtcNow },
188
- new DashboardWidget { WidgetCode = "WIDGET_WORKLOAD", WidgetName = "Workload", Description = "Team load distribution across members.", Category = "Analytics", ComponentKey = "WorkloadChart", DataSourceKey = "ProjectsAndUsers", DefaultWidth = 4, DefaultHeight = 3, DefaultOrder = 7, IsActive = true, IsDeleted = false, CreatedAt = DateTime.UtcNow },
189
- new DashboardWidget { WidgetCode = "WIDGET_AUDIT_FEED", WidgetName = "Audit Feed", Description = "Recent administrative activity.", Category = "Admin", ComponentKey = "AuditFeed", DataSourceKey = "AuditLogs", DefaultWidth = 4, DefaultHeight = 3, DefaultOrder = 8, IsActive = true, IsDeleted = false, CreatedAt = DateTime.UtcNow }
190
- };
191
-
192
- var widgetAccessTemplates = new Dictionary<string, (bool admin, bool manager, bool employee)>(StringComparer.OrdinalIgnoreCase)
193
- {
194
- ["WIDGET_GREETING_BANNER"] = (true, true, true),
195
- ["WIDGET_KPI_CARDS"] = (true, true, true),
196
- ["WIDGET_CALENDAR_STRIP"] = (true, true, true),
197
- ["WIDGET_PROJECT_PROGRESS"] = (true, true, false),
198
- ["WIDGET_TIMELINE"] = (true, true, false),
199
- ["WIDGET_TASK_TRAY"] = (true, true, true),
200
- ["WIDGET_WORKLOAD"] = (true, true, false),
201
- ["WIDGET_AUDIT_FEED"] = (true, false, false)
202
- };
203
-
204
- var roles = await db.Roles.Where(r => !r.IsDeleted).ToListAsync();
205
-
206
- foreach (var seed in widgetSeeds)
207
- {
208
- var existingWidget = await db.DashboardWidgets.FirstOrDefaultAsync(w => w.WidgetCode == seed.WidgetCode);
209
- if (existingWidget == null)
210
- {
211
- db.DashboardWidgets.Add(seed);
212
- await db.SaveChangesAsync();
213
- existingWidget = seed;
214
- }
215
- else
216
- {
217
- existingWidget.WidgetName = seed.WidgetName;
218
- existingWidget.Description = seed.Description;
219
- existingWidget.Category = seed.Category;
220
- existingWidget.ComponentKey = seed.ComponentKey;
221
- existingWidget.DataSourceKey = seed.DataSourceKey;
222
- existingWidget.DefaultWidth = seed.DefaultWidth;
223
- existingWidget.DefaultHeight = seed.DefaultHeight;
224
- existingWidget.DefaultOrder = seed.DefaultOrder;
225
- existingWidget.IsActive = seed.IsActive;
226
- existingWidget.IsDeleted = false;
227
- existingWidget.UpdatedAt = DateTime.UtcNow;
228
- }
229
-
230
- var template = widgetAccessTemplates[seed.WidgetCode];
231
- foreach (var role in roles)
232
- {
233
- var shouldView = role.Name.Equals("Admin", StringComparison.OrdinalIgnoreCase)
234
- ? template.admin
235
- : role.Name.Equals("Manager", StringComparison.OrdinalIgnoreCase)
236
- ? template.manager
237
- : template.employee;
238
-
239
- var existingAccess = await db.RoleDashboardWidgets.FirstOrDefaultAsync(row => row.RoleId == role.Id && row.WidgetId == existingWidget.WidgetId);
240
- if (existingAccess == null)
241
- {
242
- db.RoleDashboardWidgets.Add(new RoleDashboardWidget
243
- {
244
- RoleId = role.Id,
245
- WidgetId = existingWidget.WidgetId,
246
- CanView = shouldView,
247
- CanConfigure = role.Name.Equals("Admin", StringComparison.OrdinalIgnoreCase),
248
- IsDefaultVisible = shouldView,
249
- DefaultGridX = 0,
250
- DefaultGridY = 0,
251
- DefaultWidth = seed.DefaultWidth,
252
- DefaultHeight = seed.DefaultHeight,
253
- DefaultSortOrder = seed.DefaultOrder,
254
- IsDeleted = false,
255
- CreatedAt = DateTime.UtcNow
256
- });
257
- }
258
- else
259
- {
260
- existingAccess.CanView = shouldView;
261
- existingAccess.CanConfigure = role.Name.Equals("Admin", StringComparison.OrdinalIgnoreCase);
262
- existingAccess.IsDefaultVisible = shouldView;
263
- existingAccess.DefaultWidth = seed.DefaultWidth;
264
- existingAccess.DefaultHeight = seed.DefaultHeight;
265
- existingAccess.DefaultSortOrder = seed.DefaultOrder;
266
- existingAccess.IsDeleted = false;
267
- existingAccess.UpdatedAt = DateTime.UtcNow;
268
- }
269
- }
270
- }
271
-
272
  // Soft-delete existing tasks that belong to deleted projects
273
  var orphanedTasks = await db.Tasks
274
  .Where(t => t.IsDeleted != true && t.Project.IsDeleted == true)
 
128
  backlogMenu.UpdatedAt = DateTime.UtcNow;
129
  }
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  // Soft-delete existing tasks that belong to deleted projects
132
  var orphanedTasks = await db.Tasks
133
  .Where(t => t.IsDeleted != true && t.Project.IsDeleted == true)
TaskTrackingSystem.WebApp/Components/Layout/MainLayout.razor CHANGED
@@ -118,7 +118,6 @@
118
  "reports" => "Reports",
119
  "roles" => "Roles & Access",
120
  "users" => "Users",
121
- "widget-setup" => "Widget Setup",
122
  _ => "Workspace"
123
  };
124
 
 
118
  "reports" => "Reports",
119
  "roles" => "Roles & Access",
120
  "users" => "Users",
 
121
  _ => "Workspace"
122
  };
123
 
TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor CHANGED
@@ -53,10 +53,6 @@
53
  <i data-lucide="file-text" class="h-4 w-4 mr-3 text-slate-400 shrink-0"></i>
54
  <span>Audit Logs</span>
55
  </NavLink>
56
- <NavLink class="nav-link-item" href="/widget-setup" Match="NavLinkMatch.All">
57
- <i data-lucide="layout-grid" class="h-4 w-4 mr-3 text-slate-400 shrink-0"></i>
58
- <span>Widget Setup</span>
59
- </NavLink>
60
  }
61
  }
62
  else
 
53
  <i data-lucide="file-text" class="h-4 w-4 mr-3 text-slate-400 shrink-0"></i>
54
  <span>Audit Logs</span>
55
  </NavLink>
 
 
 
 
56
  }
57
  }
58
  else
TaskTrackingSystem.WebApp/Components/Pages/Home.razor CHANGED
@@ -3,12 +3,11 @@
3
  @page "/dashboard"
4
  @using Microsoft.AspNetCore.Authorization
5
  @using Microsoft.AspNetCore.Components.Authorization
6
- @using System.Security.Claims
7
  @using TaskTrackingSystem.Shared.Models.Task
8
  @using TaskTrackingSystem.Shared.Models.User
9
  @using TaskTrackingSystem.Shared.Models.Project
10
  @using TaskTrackingSystem.Shared.Models.Dashboard
11
- @using TaskTrackingSystem.Shared.Models.AuditLog
12
  @rendermode @(new InteractiveServerRenderMode(prerender: false))
13
  @attribute [Microsoft.AspNetCore.Authorization.Authorize]
14
  @inject ApiClientService ApiClient
@@ -16,268 +15,251 @@
16
  @inject AuthenticationStateProvider AuthStateProvider
17
  @inject MenuAuthorizationService MenuAuthorization
18
 
19
- <PageTitle>Dashboard</PageTitle>
20
-
21
- @if (isLoading)
22
- {
23
- <div class="flex items-center justify-center py-40">
24
- <div class="flex flex-col items-center space-y-4 text-violet-600">
25
- <svg class="h-10 w-10 animate-spin" viewBox="0 0 24 24">
26
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle>
27
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
28
- </svg>
29
- <span class="text-sm font-semibold text-slate-500">Loading dashboard widgets...</span>
30
- </div>
31
- </div>
32
- }
33
- else
34
- {
35
- <div class="space-y-6">
36
- <div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
37
  <div>
38
- <p class="text-sm font-semibold uppercase tracking-wider text-violet-600">Dashboard</p>
39
- <h2 class="mt-1 text-3xl font-bold tracking-tight text-slate-900">Welcome back, @currentUserName</h2>
40
- <p class="mt-1 text-slate-500">This dashboard is now driven only by role-aware widgets.</p>
 
 
41
  </div>
42
  <div class="flex flex-wrap gap-3">
43
  @if (canCreateTask)
44
  {
45
- <a href="/tasks" class="inline-flex items-center rounded-xl bg-violet-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-violet-700">
46
  <i data-lucide="plus" class="mr-2 h-4 w-4"></i>
47
  New Task
48
  </a>
49
  }
50
- <a href="/reports/tasks" class="inline-flex items-center rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50">
51
  <i data-lucide="bar-chart-3" class="mr-2 h-4 w-4"></i>
52
  Reports
53
  </a>
54
- @if (currentUserRole.Equals("Admin", StringComparison.OrdinalIgnoreCase))
55
- {
56
- <a href="/widget-setup" class="inline-flex items-center rounded-xl border border-violet-200 bg-violet-50 px-4 py-2.5 text-sm font-semibold text-violet-700 transition-colors hover:bg-violet-100">
57
- <i data-lucide="layout-grid" class="mr-2 h-4 w-4"></i>
58
- Widget Setup
59
- </a>
60
- }
61
  </div>
62
  </div>
 
63
 
64
- @if (!string.IsNullOrWhiteSpace(widgetLayoutMessage))
65
- {
66
- <div class="rounded-xl border border-emerald-100 bg-emerald-50 px-4 py-3">
67
- <p class="text-sm font-medium text-emerald-700">@widgetLayoutMessage</p>
 
 
 
68
  </div>
69
- }
 
 
70
 
71
- <div class="rounded-3xl border border-slate-100 bg-white shadow-sm overflow-hidden">
72
- <div class="flex flex-col gap-3 border-b border-slate-100 bg-gradient-to-r from-slate-50 to-white px-6 py-5 md:flex-row md:items-center md:justify-between">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  <div>
74
- <p class="text-xs font-semibold uppercase tracking-wider text-violet-600">Widgets</p>
75
- <h3 class="mt-1 text-lg font-bold text-slate-900">Live role-based dashboard</h3>
76
- <p class="mt-1 text-sm text-slate-500">Drag to reorder when editing is enabled.</p>
77
  </div>
78
- <div class="flex items-center gap-2">
79
- <button @onclick="ToggleWidgetEditMode" class="inline-flex items-center rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 transition-colors">
80
- <i data-lucide="grip-vertical" class="mr-2 h-4 w-4"></i>
81
- @(isWidgetEditMode ? "Done editing" : "Reorder widgets")
82
- </button>
83
- @if (isWidgetEditMode)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  {
85
- <button @onclick="SaveWidgetLayoutAsync" disabled="@isSavingWidgetLayout" class="inline-flex items-center rounded-xl bg-violet-600 px-4 py-2 text-sm font-semibold text-white hover:bg-violet-700 disabled:opacity-50 transition-colors">
86
- <i data-lucide="save" class="mr-2 h-4 w-4"></i>
87
- @(isSavingWidgetLayout ? "Saving..." : "Save layout")
88
- </button>
 
 
 
 
 
89
  }
90
  </div>
91
  </div>
 
92
 
93
- <div class="p-6">
94
- @if (dashboardWidgets.Any())
95
- {
96
- <div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
97
- @foreach (var widget in dashboardWidgets)
 
 
 
 
98
  {
99
- <div draggable="@isWidgetEditMode"
100
- @ondragstart="() => BeginWidgetDrag(widget.WidgetId)"
101
- @ondrop="() => DropWidget(widget.WidgetId)"
102
- @ondragover:preventDefault="true"
103
- class="rounded-2xl border border-slate-100 bg-slate-50/70 p-5 shadow-sm transition-all @(GetWidgetCardClasses(widget))">
104
- <div class="flex items-start justify-between gap-4">
105
- <div class="min-w-0">
106
- <p class="text-[10px] font-bold uppercase tracking-wider text-slate-400">@DisplayWidgetCategory(widget.Category)</p>
107
- <h4 class="mt-1 truncate text-base font-bold text-slate-900">@widget.WidgetName</h4>
108
- <p class="mt-1 line-clamp-2 text-sm text-slate-500">@(widget.Description ?? "No description provided.")</p>
109
- </div>
110
- <div class="flex flex-col items-end gap-2">
111
- <span class="rounded-full border border-slate-200 bg-white px-2.5 py-1 text-[10px] font-semibold text-slate-500">@widget.ComponentKey</span>
112
- @if (widget.CanConfigure)
113
- {
114
- <span class="rounded-full bg-violet-50 px-2.5 py-1 text-[10px] font-semibold text-violet-700">Configurable</span>
115
- }
116
- </div>
117
- </div>
118
-
119
- <div class="mt-4 flex flex-wrap gap-2">
120
- <span class="rounded-full border border-slate-200 bg-white px-3 py-1 text-[10px] font-semibold text-slate-500">Size @widget.Width x @widget.Height</span>
121
- <span class="rounded-full border border-slate-200 bg-white px-3 py-1 text-[10px] font-semibold text-slate-500">Order @widget.SortOrder</span>
122
- @if (widget.IsPinned)
123
- {
124
- <span class="rounded-full bg-emerald-50 px-3 py-1 text-[10px] font-semibold text-emerald-700">Pinned</span>
125
- }
126
- @if (widget.IsHidden)
127
- {
128
- <span class="rounded-full bg-slate-100 px-3 py-1 text-[10px] font-semibold text-slate-500">Hidden</span>
129
- }
130
- </div>
131
-
132
- <div class="mt-4 rounded-xl border border-slate-200 bg-white p-4">
133
- @if (widget.ComponentKey == "GreetingBanner")
134
- {
135
- <div class="flex items-center justify-between gap-4">
136
- <div>
137
- <p class="text-sm font-semibold text-slate-900">Good @GetGreetingPeriod(), @currentUserName</p>
138
- <p class="mt-1 text-xs text-slate-500">@DateTime.Now.ToString("dddd, MMMM d, yyyy h:mm tt")</p>
139
- </div>
140
- <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-violet-50 font-bold text-violet-700">
141
- @(currentUserName.Length > 0 ? currentUserName[0].ToString().ToUpper() : "U")
142
- </div>
143
- </div>
144
- }
145
- else if (widget.ComponentKey == "KpiCards")
146
- {
147
- <div class="grid grid-cols-3 gap-3">
148
- <div class="rounded-xl bg-violet-50 p-3">
149
- <p class="text-[10px] font-semibold uppercase text-violet-600">Users</p>
150
- <p class="mt-1 text-2xl font-bold text-violet-900">@summary.TotalUsers</p>
151
- </div>
152
- <div class="rounded-xl bg-emerald-50 p-3">
153
- <p class="text-[10px] font-semibold uppercase text-emerald-600">Projects</p>
154
- <p class="mt-1 text-2xl font-bold text-emerald-900">@summary.ActiveProjectsCount</p>
155
- </div>
156
- <div class="rounded-xl bg-blue-50 p-3">
157
- <p class="text-[10px] font-semibold uppercase text-blue-600">Tasks</p>
158
- <p class="mt-1 text-2xl font-bold text-blue-900">@summary.PendingTasksCount</p>
159
- </div>
160
- </div>
161
- }
162
- else if (widget.ComponentKey == "ProjectProgressChart")
163
- {
164
- <div class="space-y-3">
165
- @foreach (var pp in projectProgress.Take(3))
166
- {
167
- <div>
168
- <div class="flex items-center justify-between text-sm">
169
- <span class="truncate pr-4 font-semibold text-slate-800">@pp.ProjectName</span>
170
- <span class="text-slate-500">@pp.CompletedTasksCount / @pp.TotalTasksCount</span>
171
- </div>
172
- <div class="mt-2 h-2 overflow-hidden rounded-full bg-slate-100">
173
- <div class="h-full rounded-full bg-violet-600" style="width: @pp.CompletionPercentage%"></div>
174
- </div>
175
- </div>
176
- }
177
- </div>
178
- }
179
- else if (widget.ComponentKey == "AuditFeed")
180
- {
181
- @if (auditLogs.Any())
182
- {
183
- <div class="space-y-2">
184
- @foreach (var log in auditLogs.Take(5))
185
- {
186
- <div class="flex items-start justify-between gap-3 rounded-xl bg-slate-50 p-3">
187
- <div class="min-w-0">
188
- <p class="truncate text-sm font-semibold text-slate-800">@log.Action @log.Module</p>
189
- <p class="line-clamp-1 text-xs text-slate-500">@log.Description</p>
190
- </div>
191
- <span class="shrink-0 text-[10px] text-slate-400">@log.CreatedAt.ToString("MMM d, HH:mm")</span>
192
- </div>
193
- }
194
- </div>
195
- }
196
- else
197
- {
198
- <p class="text-sm text-slate-500">No audit logs available yet.</p>
199
- }
200
- }
201
- else if (widget.ComponentKey == "TimelineChart")
202
- {
203
- <div class="space-y-2">
204
- @foreach (var project in projectsList.OrderBy(p => p.StartDate).Take(3))
205
- {
206
- <div class="rounded-xl border border-slate-100 bg-slate-50 p-3">
207
- <div class="flex items-center justify-between gap-2">
208
- <p class="truncate text-sm font-semibold text-slate-800">@project.Name</p>
209
- <span class="text-[10px] text-slate-500">@DisplayFormats.Date(project.StartDate) - @DisplayFormats.Date(project.EndDate)</span>
210
- </div>
211
- </div>
212
- }
213
- </div>
214
- }
215
- else if (widget.ComponentKey == "TaskTray")
216
- {
217
- <div class="space-y-2">
218
- @foreach (var task in taskList.Where(t => t.StatusId != AppTaskStatus.Done).OrderBy(t => t.DueDate).Take(4))
219
- {
220
- <div class="rounded-xl bg-slate-50 p-3">
221
- <p class="truncate text-sm font-semibold text-slate-800">@task.Title</p>
222
- <p class="mt-1 text-xs text-slate-500">Due @task.DueDate.ToString("MMM d")</p>
223
- </div>
224
- }
225
- </div>
226
- }
227
- else if (widget.ComponentKey == "WorkloadChart")
228
- {
229
- <div class="space-y-3">
230
- @foreach (var team in GetTeamWorkload())
231
- {
232
- <div>
233
- <div class="flex items-center justify-between text-sm">
234
- <span class="font-semibold text-slate-800">@team.Name</span>
235
- <span class="text-slate-500">@team.TaskCount tasks</span>
236
- </div>
237
- <div class="mt-2 h-2 overflow-hidden rounded-full bg-slate-100">
238
- <div class="h-full rounded-full bg-emerald-500" style="width: @team.BarWidth%"></div>
239
- </div>
240
- </div>
241
- }
242
- </div>
243
- }
244
- else
245
- {
246
- <p class="text-sm text-slate-500">Widget preview for <span class="font-semibold text-slate-900">@widget.ComponentKey</span> is not configured yet.</p>
247
- }
248
- </div>
249
- </div>
250
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  </div>
252
  }
253
- else
 
 
 
 
 
 
 
 
254
  {
255
- <div class="rounded-xl border border-dashed border-slate-200 bg-slate-50 p-10 text-center">
256
- <p class="text-sm font-semibold text-slate-700">No widgets available for your role</p>
257
- <p class="mt-1 text-sm text-slate-500">Ask an administrator to enable widgets for this role.</p>
 
 
 
 
 
258
  </div>
259
  }
260
  </div>
 
 
 
 
261
  </div>
262
  </div>
263
- }
264
 
265
  @code {
266
- private bool isLoading = true;
267
  private DashboardSummaryDto summary = new();
 
268
  private List<ProjectProgressDto> projectProgress = new();
269
- private List<DashboardWidgetDto> dashboardWidgets = new();
270
- private List<AuditLogDto> auditLogs = new();
271
  private List<TaskDto> taskList = new();
272
  private List<UserDto> usersList = new();
273
  private List<ProjectDto> projectsList = new();
274
  private string currentUserName = "User";
275
- private string currentUserRole = "Member";
276
  private bool canCreateTask = false;
277
- private bool isWidgetEditMode = false;
278
- private bool isSavingWidgetLayout = false;
279
- private long? draggedWidgetId;
280
- private string? widgetLayoutMessage;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
  protected override async Task OnInitializedAsync()
283
  {
@@ -294,29 +276,22 @@ else
294
  var authState = await AuthStateProvider.GetAuthenticationStateAsync();
295
  var user = authState.User;
296
  currentUserName = user.Identity?.Name ?? "User";
297
- currentUserRole = user.FindFirst(ClaimTypes.Role)?.Value ?? "Member";
298
-
299
  var summaryResult = await client.GetFromJsonAsync<Result<DashboardSummaryDto>>("Dashboard/summary", Serialization.CaseInsensitive);
300
  if (summaryResult?.IsSuccess == true && summaryResult.Value != null)
301
  {
302
  summary = summaryResult.Value;
303
  }
304
 
305
- var progressResult = await client.GetFromJsonAsync<Result<List<ProjectProgressDto>>>("Dashboard/project-progress", Serialization.CaseInsensitive);
306
- if (progressResult?.IsSuccess == true && progressResult.Value != null)
307
  {
308
- projectProgress = progressResult.Value;
309
  }
310
 
311
- var widgetsResult = await client.GetFromJsonAsync<Result<List<DashboardWidgetDto>>>("Dashboard/widgets", Serialization.CaseInsensitive);
312
- if (widgetsResult?.IsSuccess == true && widgetsResult.Value != null)
313
- {
314
- dashboardWidgets = widgetsResult.Value;
315
- }
316
-
317
- if (user.IsInRole("Admin"))
318
  {
319
- auditLogs = await client.GetFromJsonAsync<List<AuditLogDto>>("AuditLog", Serialization.CaseInsensitive) ?? new();
320
  }
321
 
322
  taskList = await client.GetFromJsonAsync<List<TaskDto>>("Task", Serialization.CaseInsensitive) ?? new();
@@ -325,11 +300,7 @@ else
325
  }
326
  catch (Exception ex)
327
  {
328
- Console.WriteLine($"Dashboard widgets load error: {ex.Message}");
329
- }
330
- finally
331
- {
332
- isLoading = false;
333
  }
334
  }
335
 
@@ -338,141 +309,166 @@ else
338
  await JS.InvokeVoidAsync("initIcons");
339
  }
340
 
341
- private string DisplayWidgetCategory(string? category) =>
342
- string.IsNullOrWhiteSpace(category) ? "General" : category;
343
 
344
- private string GetGreetingPeriod()
345
  {
346
- var hour = DateTime.Now.Hour;
347
- if (hour < 12) return "morning";
348
- if (hour < 17) return "afternoon";
349
- return "evening";
350
- }
351
-
352
- private IEnumerable<(string Name, int TaskCount, int BarWidth)> GetTeamWorkload()
353
- {
354
- var relevantUsers = usersList
355
- .Where(u => !u.Username.Equals(currentUserName, StringComparison.OrdinalIgnoreCase))
356
- .Take(6)
357
- .ToList();
358
 
359
- var maxTasks = Math.Max(taskList.Count, 1);
360
- return relevantUsers.Select(user =>
361
  {
362
- var taskCount = taskList.Count(t => t.AssignedTo == user.Id);
363
- var width = Math.Max(10, (int)Math.Round((double)taskCount / maxTasks * 100));
364
- return (Name: $"{user.FirstName} {user.LastName}", TaskCount: taskCount, BarWidth: width);
365
- });
 
 
 
 
366
  }
367
 
368
- private string GetWidgetCardClasses(DashboardWidgetDto widget)
369
  {
370
- var classes = new List<string>
371
  {
372
- "border-slate-100",
373
- widget.IsHidden ? "opacity-75 bg-slate-100/70" : "bg-slate-50/70"
374
- };
 
 
 
 
 
 
 
 
375
 
376
- classes.Add(isWidgetEditMode ? "cursor-grab hover:shadow-md" : "cursor-default");
 
377
 
378
- if (draggedWidgetId == widget.WidgetId)
379
  {
380
- classes.Add("ring-2 ring-violet-300");
381
  }
382
 
383
- return string.Join(" ", classes);
384
- }
385
-
386
- private void ToggleWidgetEditMode()
387
- {
388
- isWidgetEditMode = !isWidgetEditMode;
389
- widgetLayoutMessage = null;
390
- draggedWidgetId = null;
391
- }
392
 
393
- private void BeginWidgetDrag(long widgetId)
394
- {
395
- if (!isWidgetEditMode)
396
  {
397
- return;
398
  }
399
 
400
- draggedWidgetId = widgetId;
401
  }
402
 
403
- private void DropWidget(long targetWidgetId)
404
  {
405
- if (!isWidgetEditMode || draggedWidgetId == null || draggedWidgetId == targetWidgetId)
 
 
406
  {
407
- draggedWidgetId = null;
408
- return;
409
  }
410
 
411
- var fromIndex = dashboardWidgets.FindIndex(widget => widget.WidgetId == draggedWidgetId.Value);
412
- var toIndex = dashboardWidgets.FindIndex(widget => widget.WidgetId == targetWidgetId);
413
- if (fromIndex < 0 || toIndex < 0)
414
- {
415
- draggedWidgetId = null;
416
- return;
417
- }
418
 
419
- var moved = dashboardWidgets[fromIndex];
420
- dashboardWidgets.RemoveAt(fromIndex);
421
- if (fromIndex < toIndex)
422
  {
423
- toIndex--;
424
  }
425
- dashboardWidgets.Insert(toIndex, moved);
426
- draggedWidgetId = null;
427
  }
428
 
429
- private async Task SaveWidgetLayoutAsync()
430
  {
431
- if (isSavingWidgetLayout)
432
  {
433
- return;
434
- }
 
 
 
435
 
436
- isSavingWidgetLayout = true;
437
- widgetLayoutMessage = null;
 
 
 
 
 
438
 
439
- try
 
440
  {
441
- var client = ApiClient.CreateClient();
442
- var request = new DashboardWidgetLayoutSaveRequestDto
443
- {
444
- Widgets = dashboardWidgets.Select((widget, index) => new DashboardWidgetLayoutItemDto
445
- {
446
- WidgetId = widget.WidgetId,
447
- GridX = 0,
448
- GridY = index,
449
- Width = widget.Width > 0 ? widget.Width : widget.DefaultWidth,
450
- Height = widget.Height > 0 ? widget.Height : widget.DefaultHeight,
451
- SortOrder = index,
452
- IsHidden = widget.IsHidden,
453
- IsPinned = widget.IsPinned,
454
- ConfigJson = widget.ConfigJson
455
- }).ToList()
456
- };
457
-
458
- var response = await client.PutAsJsonAsync("Dashboard/widgets/layout", request);
459
- if (response.IsSuccessStatusCode)
460
- {
461
- widgetLayoutMessage = "Widget layout saved.";
462
- isWidgetEditMode = false;
463
- }
464
- else
465
- {
466
- widgetLayoutMessage = $"Failed to save layout. {(int)response.StatusCode} {response.ReasonPhrase}";
467
- }
468
  }
469
- catch (Exception ex)
 
 
 
 
470
  {
471
- widgetLayoutMessage = $"Failed to save layout: {ex.Message}";
 
 
 
 
 
472
  }
473
- finally
 
 
 
 
 
474
  {
475
- isSavingWidgetLayout = false;
476
- }
 
 
 
477
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  }
 
3
  @page "/dashboard"
4
  @using Microsoft.AspNetCore.Authorization
5
  @using Microsoft.AspNetCore.Components.Authorization
 
6
  @using TaskTrackingSystem.Shared.Models.Task
7
  @using TaskTrackingSystem.Shared.Models.User
8
  @using TaskTrackingSystem.Shared.Models.Project
9
  @using TaskTrackingSystem.Shared.Models.Dashboard
10
+ @using TaskTrackingSystem.Shared.Enums
11
  @rendermode @(new InteractiveServerRenderMode(prerender: false))
12
  @attribute [Microsoft.AspNetCore.Authorization.Authorize]
13
  @inject ApiClientService ApiClient
 
15
  @inject AuthenticationStateProvider AuthStateProvider
16
  @inject MenuAuthorizationService MenuAuthorization
17
 
18
+ <PageTitle>Overview</PageTitle>
19
+
20
+ <div class="space-y-8 text-slate-900">
21
+ <div class="rounded-[32px] border border-slate-200 bg-white px-8 py-8 text-slate-900 shadow-sm shadow-slate-200/60">
22
+ <div class="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  <div>
24
+ <p class="text-sm font-semibold uppercase tracking-[0.22em] text-violet-600">Overview</p>
25
+ <h2 class="mt-2 text-4xl font-bold tracking-tight text-slate-900">Welcome back, @currentUserName</h2>
26
+ <p class="mt-3 max-w-2xl text-sm text-slate-600">
27
+ Projects, tasks, and team activity at a glance.
28
+ </p>
29
  </div>
30
  <div class="flex flex-wrap gap-3">
31
  @if (canCreateTask)
32
  {
33
+ <a href="/tasks/all" class="inline-flex items-center rounded-2xl bg-violet-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-violet-900/20 transition-colors hover:bg-violet-500">
34
  <i data-lucide="plus" class="mr-2 h-4 w-4"></i>
35
  New Task
36
  </a>
37
  }
38
+ <a href="/reports/tasks" class="inline-flex items-center rounded-2xl border border-slate-200 bg-white px-5 py-3 text-sm font-semibold text-slate-700 shadow-sm transition-colors hover:bg-slate-50">
39
  <i data-lucide="bar-chart-3" class="mr-2 h-4 w-4"></i>
40
  Reports
41
  </a>
 
 
 
 
 
 
 
42
  </div>
43
  </div>
44
+ </div>
45
 
46
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
47
+ <div class="rounded-[24px] border border-slate-200 bg-white p-5 text-slate-900 shadow-sm shadow-slate-200/50">
48
+ <div class="flex items-center justify-between">
49
+ <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-violet-500 text-white">
50
+ <i data-lucide="users" class="h-5 w-5"></i>
51
+ </div>
52
+ <span class="text-sm font-semibold text-emerald-500">+12 this month</span>
53
  </div>
54
+ <p class="mt-5 text-4xl font-bold">@summary.TotalUsers</p>
55
+ <p class="mt-2 text-sm text-slate-500">Total users</p>
56
+ </div>
57
 
58
+ <div class="rounded-[24px] border border-slate-200 bg-white p-5 text-slate-900 shadow-sm shadow-slate-200/50">
59
+ <div class="flex items-center justify-between">
60
+ <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-sky-500 text-white">
61
+ <i data-lucide="folder-kanban" class="h-5 w-5"></i>
62
+ </div>
63
+ <span class="text-sm font-semibold text-emerald-500">+3 this month</span>
64
+ </div>
65
+ <p class="mt-5 text-4xl font-bold">@summary.ActiveProjectsCount</p>
66
+ <p class="mt-2 text-sm text-slate-500">Total projects</p>
67
+ </div>
68
+
69
+ <div class="rounded-[24px] border border-slate-200 bg-white p-5 text-slate-900 shadow-sm shadow-slate-200/50">
70
+ <div class="flex items-center justify-between">
71
+ <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-500 text-white">
72
+ <i data-lucide="check-square" class="h-5 w-5"></i>
73
+ </div>
74
+ <span class="text-sm font-semibold text-emerald-500">+24 this week</span>
75
+ </div>
76
+ <p class="mt-5 text-4xl font-bold">@ActiveTasksCount</p>
77
+ <p class="mt-2 text-sm text-slate-500">Active tasks</p>
78
+ </div>
79
+
80
+ <div class="rounded-[24px] border border-slate-200 bg-white p-5 text-slate-900 shadow-sm shadow-slate-200/50">
81
+ <div class="flex items-center justify-between">
82
+ <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-amber-500 text-white">
83
+ <i data-lucide="trending-up" class="h-5 w-5"></i>
84
+ </div>
85
+ <span class="text-sm font-semibold text-emerald-500">+5% vs last mo</span>
86
+ </div>
87
+ <p class="mt-5 text-4xl font-bold">@CompletionRate%</p>
88
+ <p class="mt-2 text-sm text-slate-500">Completion rate</p>
89
+ </div>
90
+ </div>
91
+
92
+ <div class="grid grid-cols-1 gap-5 xl:grid-cols-[minmax(0,1.7fr)_minmax(340px,1fr)]">
93
+ <div class="rounded-[28px] border border-slate-200 bg-white p-6 text-slate-900 shadow-sm shadow-slate-200/60">
94
+ <div class="flex items-start justify-between gap-4">
95
  <div>
96
+ <h3 class="text-2xl font-bold text-slate-900">Project Activity</h3>
97
+ <p class="mt-1 text-sm text-slate-500">Created and completed tasks over the last six months.</p>
 
98
  </div>
99
+ <div class="text-sm text-slate-500">@ActivityRangeLabel</div>
100
+ </div>
101
+
102
+ <div class="mt-6">
103
+ <svg viewBox="0 0 760 340" class="h-[340px] w-full">
104
+ <defs>
105
+ <linearGradient id="activityFill" x1="0" x2="0" y1="0" y2="1">
106
+ <stop offset="0%" stop-color="#7c3aed" stop-opacity="0.18" />
107
+ <stop offset="100%" stop-color="#7c3aed" stop-opacity="0" />
108
+ </linearGradient>
109
+ </defs>
110
+
111
+ @for (var i = 0; i < 5; i++)
112
+ {
113
+ var y = 60 + (i * 55);
114
+ <line x1="48" y1="@y" x2="730" y2="@y" stroke="#e2e8f0" stroke-width="1" />
115
+ }
116
+
117
+ @for (var i = 0; i < 6; i++)
118
+ {
119
+ var x = 60 + (i * 125);
120
+ <line x1="@x" y1="40" x2="@x" y2="280" stroke="#e2e8f0" stroke-width="1" />
121
+ }
122
+
123
+ <path d="@ActivityAreaPath" fill="url(#activityFill)" />
124
+ <path d="@ActivityLinePath" fill="none" stroke="#7c3aed" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
125
+
126
+ @foreach (var point in ActivityPoints)
127
  {
128
+ <circle cx="@point.X" cy="@point.Y" r="5" fill="#7c3aed" stroke="#ddd6fe" stroke-width="3" />
129
+ }
130
+
131
+ </svg>
132
+
133
+ <div class="mt-4 flex items-center justify-between text-xs text-slate-500">
134
+ @foreach (var label in ActivityLabels)
135
+ {
136
+ <span class="w-1/6 text-center">@label</span>
137
  }
138
  </div>
139
  </div>
140
+ </div>
141
 
142
+ <div class="rounded-[28px] border border-slate-200 bg-white p-6 text-slate-900 shadow-sm shadow-slate-200/60">
143
+ <h3 class="text-2xl font-bold text-slate-900">Task Distribution</h3>
144
+ <p class="mt-1 text-sm text-slate-500">@TotalTasksCount total tasks</p>
145
+
146
+ <div class="mt-8 flex justify-center">
147
+ <div class="relative">
148
+ <svg viewBox="0 0 220 220" class="h-56 w-56">
149
+ <circle cx="110" cy="110" r="74" fill="none" stroke="#e5e7eb" stroke-width="24" />
150
+ @foreach (var slice in DonutSlices)
151
  {
152
+ <circle cx="110" cy="110" r="74"
153
+ fill="none"
154
+ stroke="@slice.Color"
155
+ stroke-width="24"
156
+ stroke-linecap="round"
157
+ stroke-dasharray="@slice.DashArray"
158
+ stroke-dashoffset="@slice.DashOffset"
159
+ transform="rotate(-90 110 110)" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  }
161
+ </svg>
162
+ <div class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center">
163
+ <div class="text-3xl font-bold text-slate-900">@TotalTasksCount</div>
164
+ <div class="text-xs text-slate-500">tasks</div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <div class="mt-3 space-y-3">
170
+ @foreach (var item in DistributionLegend)
171
+ {
172
+ <div class="flex items-center justify-between rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3">
173
+ <div class="flex items-center gap-3">
174
+ <span class="h-3 w-3 rounded-full" style="background-color: @item.Color"></span>
175
+ <span class="text-sm text-slate-600">@item.Label</span>
176
+ </div>
177
+ <span class="text-sm font-semibold text-slate-900">@item.Count</span>
178
+ </div>
179
+ }
180
+ </div>
181
+ </div>
182
+ </div>
183
+
184
+ <div class="grid grid-cols-1 gap-5 xl:grid-cols-3">
185
+ <div class="rounded-[28px] border border-slate-200 bg-white p-6 text-slate-900 shadow-sm shadow-slate-200/60 xl:col-span-2">
186
+ <div class="flex items-center justify-between">
187
+ <div>
188
+ <h3 class="text-xl font-bold text-slate-900">Recent Projects</h3>
189
+ <p class="mt-1 text-sm text-slate-500">What the team is working on right now.</p>
190
+ </div>
191
+ </div>
192
+
193
+ <div class="mt-5 space-y-3">
194
+ @foreach (var project in projectsList.OrderByDescending(p => p.CreatedAt).Take(4))
195
+ {
196
+ <div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4">
197
+ <div class="flex items-center justify-between gap-4">
198
+ <div class="min-w-0">
199
+ <p class="truncate text-sm font-semibold text-slate-900">@project.Name</p>
200
+ <p class="mt-1 text-xs text-slate-500">@DisplayFormats.Date(project.StartDate) to @DisplayFormats.Date(project.EndDate)</p>
201
+ </div>
202
+ <span class="text-xs font-semibold text-slate-600">@TaskCountForProject(project.Id) tasks</span>
203
+ </div>
204
  </div>
205
  }
206
+ </div>
207
+ </div>
208
+
209
+ <div class="rounded-[28px] border border-slate-200 bg-white p-6 text-slate-900 shadow-sm shadow-slate-200/60">
210
+ <h3 class="text-xl font-bold text-slate-900">Team Snapshot</h3>
211
+ <p class="mt-1 text-sm text-slate-500">People with the most assigned work.</p>
212
+
213
+ <div class="mt-5 space-y-4">
214
+ @foreach (var person in TeamSnapshot)
215
  {
216
+ <div>
217
+ <div class="flex items-center justify-between text-sm">
218
+ <span class="font-semibold text-slate-900">@person.Name</span>
219
+ <span class="text-slate-500">@person.Count tasks</span>
220
+ </div>
221
+ <div class="mt-2 h-2 overflow-hidden rounded-full bg-slate-200">
222
+ <div class="h-full rounded-full bg-emerald-500" style="width: @person.BarWidth%"></div>
223
+ </div>
224
  </div>
225
  }
226
  </div>
227
+
228
+ <a href="/users" class="mt-5 inline-flex w-full items-center justify-center rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm font-semibold text-slate-700 shadow-sm transition-colors hover:bg-slate-50">
229
+ View team
230
+ </a>
231
  </div>
232
  </div>
233
+ </div>
234
 
235
  @code {
 
236
  private DashboardSummaryDto summary = new();
237
+ private List<TaskStatusOverviewDto> taskStatuses = new();
238
  private List<ProjectProgressDto> projectProgress = new();
 
 
239
  private List<TaskDto> taskList = new();
240
  private List<UserDto> usersList = new();
241
  private List<ProjectDto> projectsList = new();
242
  private string currentUserName = "User";
 
243
  private bool canCreateTask = false;
244
+
245
+ private List<string> ActivityLabels => ActivityMonths.Select(m => m.Label).ToList();
246
+
247
+ private IReadOnlyList<ActivityMonth> ActivityMonths =>
248
+ GetActivityMonths();
249
+
250
+ private string ActivityRangeLabel =>
251
+ $"{ActivityMonths.First().Label} - {ActivityMonths.Last().Label}";
252
+
253
+ private string ActivityLinePath => BuildActivityPath(ActivityMonths, false);
254
+ private string ActivityAreaPath => BuildActivityPath(ActivityMonths, true);
255
+ private IReadOnlyList<ActivityPoint> ActivityPoints => BuildActivityPoints();
256
+ private IReadOnlyList<DistributionSlice> DonutSlices => BuildDonutSlices();
257
+ private IReadOnlyList<DistributionItem> DistributionLegend => BuildDistributionLegend();
258
+ private int TotalTasksCount => taskList.Count;
259
+ private int ActiveTasksCount => taskList.Count(t => t.StatusId != AppTaskStatus.Done);
260
+ private int CompletedTasksCount => taskList.Count(t => t.StatusId == AppTaskStatus.Done);
261
+ private int CompletionRate => TotalTasksCount > 0 ? (int)Math.Round((double)CompletedTasksCount / TotalTasksCount * 100) : 0;
262
+ private IEnumerable<(string Name, int Count, int BarWidth)> TeamSnapshot => BuildTeamSnapshot();
263
 
264
  protected override async Task OnInitializedAsync()
265
  {
 
276
  var authState = await AuthStateProvider.GetAuthenticationStateAsync();
277
  var user = authState.User;
278
  currentUserName = user.Identity?.Name ?? "User";
 
 
279
  var summaryResult = await client.GetFromJsonAsync<Result<DashboardSummaryDto>>("Dashboard/summary", Serialization.CaseInsensitive);
280
  if (summaryResult?.IsSuccess == true && summaryResult.Value != null)
281
  {
282
  summary = summaryResult.Value;
283
  }
284
 
285
+ var tasksOverviewResult = await client.GetFromJsonAsync<Result<List<TaskStatusOverviewDto>>>("Dashboard/tasks-overview", Serialization.CaseInsensitive);
286
+ if (tasksOverviewResult?.IsSuccess == true && tasksOverviewResult.Value != null)
287
  {
288
+ taskStatuses = tasksOverviewResult.Value;
289
  }
290
 
291
+ var progressResult = await client.GetFromJsonAsync<Result<List<ProjectProgressDto>>>("Dashboard/project-progress", Serialization.CaseInsensitive);
292
+ if (progressResult?.IsSuccess == true && progressResult.Value != null)
 
 
 
 
 
293
  {
294
+ projectProgress = progressResult.Value;
295
  }
296
 
297
  taskList = await client.GetFromJsonAsync<List<TaskDto>>("Task", Serialization.CaseInsensitive) ?? new();
 
300
  }
301
  catch (Exception ex)
302
  {
303
+ Console.WriteLine($"Dashboard load error: {ex.Message}");
 
 
 
 
304
  }
305
  }
306
 
 
309
  await JS.InvokeVoidAsync("initIcons");
310
  }
311
 
312
+ private static string GetMonthLabel(DateTime date) => date.ToString("MMM");
 
313
 
314
+ private IReadOnlyList<ActivityMonth> GetActivityMonths()
315
  {
316
+ var months = new List<ActivityMonth>();
317
+ var now = DateTime.Today;
318
+ var start = new DateTime(now.Year, now.Month, 1).AddMonths(-5);
 
 
 
 
 
 
 
 
 
319
 
320
+ for (var i = 0; i < 6; i++)
 
321
  {
322
+ var monthStart = start.AddMonths(i);
323
+ var nextMonth = monthStart.AddMonths(1);
324
+ var created = taskList.Count(task => task.CreatedAt >= monthStart && task.CreatedAt < nextMonth);
325
+ var completed = taskList.Count(task => task.CompletedAt.HasValue && task.CompletedAt.Value >= monthStart && task.CompletedAt.Value < nextMonth);
326
+ months.Add(new ActivityMonth(GetMonthLabel(monthStart), created, completed));
327
+ }
328
+
329
+ return months;
330
  }
331
 
332
+ private string BuildActivityPath(IReadOnlyList<ActivityMonth> months, bool filled)
333
  {
334
+ if (months.Count == 0)
335
  {
336
+ return string.Empty;
337
+ }
338
+
339
+ const double left = 48;
340
+ const double right = 730;
341
+ const double top = 48;
342
+ const double bottom = 280;
343
+ const double maxValue = 20;
344
+
345
+ double X(int i) => left + (i * ((right - left) / (months.Count - 1)));
346
+ double Y(double value) => bottom - Math.Min(value, maxValue) / maxValue * (bottom - top);
347
 
348
+ var values = months.Select(m => (double)m.Created).ToList();
349
+ var points = new List<string>();
350
 
351
+ for (var i = 0; i < months.Count; i++)
352
  {
353
+ points.Add($"{X(i):0},{Y(values[i]):0}");
354
  }
355
 
356
+ var linePath = $"M {points[0]}";
357
+ if (points.Count > 1)
358
+ {
359
+ for (var i = 1; i < points.Count; i++)
360
+ {
361
+ linePath += $" L {points[i]}";
362
+ }
363
+ }
 
364
 
365
+ if (!filled)
 
 
366
  {
367
+ return linePath;
368
  }
369
 
370
+ return $"{linePath} L {X(months.Count - 1):0},{bottom:0} L {X(0):0},{bottom:0} Z";
371
  }
372
 
373
+ private IReadOnlyList<ActivityPoint> BuildActivityPoints()
374
  {
375
+ var points = new List<ActivityPoint>();
376
+ var months = ActivityMonths;
377
+ if (months.Count == 0)
378
  {
379
+ return points;
 
380
  }
381
 
382
+ const double left = 48;
383
+ const double right = 730;
384
+ const double top = 48;
385
+ const double bottom = 280;
386
+ var maxValue = Math.Max(months.Max(m => m.Created), 1);
387
+ double X(int i) => left + (i * ((right - left) / (months.Count - 1)));
388
+ double Y(double value) => bottom - Math.Min(value, maxValue) / (double)maxValue * (bottom - top);
389
 
390
+ for (var i = 0; i < months.Count; i++)
 
 
391
  {
392
+ points.Add(new ActivityPoint(X(i), Y(months[i].Created)));
393
  }
394
+
395
+ return points;
396
  }
397
 
398
+ private IReadOnlyList<DistributionSlice> BuildDonutSlices()
399
  {
400
+ var colors = new[]
401
  {
402
+ "#10b981",
403
+ "#8b5cf6",
404
+ "#06b6d4",
405
+ "#ef4444"
406
+ };
407
 
408
+ var counts = new[]
409
+ {
410
+ CompletedTasksCount,
411
+ taskStatuses.FirstOrDefault(s => s.StatusId == AppTaskStatus.InProgress)?.TaskCount ?? 0,
412
+ taskStatuses.FirstOrDefault(s => s.StatusId == AppTaskStatus.Todo)?.TaskCount ?? 0,
413
+ Math.Max(taskList.Count(t => t.DueDate.Date < DateTime.Today && t.StatusId != AppTaskStatus.Done), 0)
414
+ };
415
 
416
+ var total = counts.Sum();
417
+ if (total <= 0)
418
  {
419
+ total = 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  }
421
+
422
+ var circumference = 2 * Math.PI * 74;
423
+ var slices = new List<DistributionSlice>();
424
+ var offset = 0.0;
425
+ for (var i = 0; i < counts.Length; i++)
426
  {
427
+ var value = counts[i];
428
+ var fraction = value / (double)total;
429
+ var dash = $"{circumference * fraction:0.##} {circumference:0.##}";
430
+ var dashOffset = circumference - (circumference * offset);
431
+ slices.Add(new DistributionSlice(colors[i], dash, dashOffset));
432
+ offset += fraction;
433
  }
434
+ return slices;
435
+ }
436
+
437
+ private IReadOnlyList<DistributionItem> BuildDistributionLegend()
438
+ {
439
+ return new List<DistributionItem>
440
  {
441
+ new("Completed", CompletedTasksCount, "#10b981"),
442
+ new("In Progress", taskStatuses.FirstOrDefault(s => s.StatusId == AppTaskStatus.InProgress)?.TaskCount ?? 0, "#8b5cf6"),
443
+ new("To Do", taskStatuses.FirstOrDefault(s => s.StatusId == AppTaskStatus.Todo)?.TaskCount ?? 0, "#06b6d4"),
444
+ new("Overdue", taskList.Count(t => t.DueDate.Date < DateTime.Today && t.StatusId != AppTaskStatus.Done), "#ef4444")
445
+ };
446
  }
447
+
448
+ private IEnumerable<(string Name, int Count, int BarWidth)> BuildTeamSnapshot()
449
+ {
450
+ var relevantUsers = usersList
451
+ .Where(u => !u.Username.Equals(currentUserName, StringComparison.OrdinalIgnoreCase))
452
+ .Take(5)
453
+ .ToList();
454
+
455
+ var maxTasks = Math.Max(taskList.Count, 1);
456
+ return relevantUsers
457
+ .Select(user =>
458
+ {
459
+ var count = taskList.Count(task => task.AssignedTo == user.Id);
460
+ var barWidth = Math.Max(10, (int)Math.Round((double)count / maxTasks * 100));
461
+ return (Name: $"{user.FirstName} {user.LastName}", Count: count, BarWidth: barWidth);
462
+ })
463
+ .OrderByDescending(item => item.Count)
464
+ .ToList();
465
+ }
466
+
467
+ private int TaskCountForProject(long projectId) =>
468
+ taskList.Count(task => task.ProjectId == projectId);
469
+
470
+ private readonly record struct ActivityMonth(string Label, int Created, int Completed);
471
+ private readonly record struct ActivityPoint(double X, double Y);
472
+ private readonly record struct DistributionSlice(string Color, string DashArray, double DashOffset);
473
+ private readonly record struct DistributionItem(string Label, int Count, string Color);
474
  }
TaskTrackingSystem.WebApp/Components/Pages/WidgetSetup.razor DELETED
@@ -1,800 +0,0 @@
1
- @page "/widget-setup"
2
- @using Microsoft.AspNetCore.Authorization
3
- @using Microsoft.AspNetCore.Components.Authorization
4
- @using TaskTrackingSystem.Shared.Models.Dashboard
5
- @using TaskTrackingSystem.Shared.Models.Role
6
- @rendermode @(new InteractiveServerRenderMode(prerender: false))
7
- @attribute [Microsoft.AspNetCore.Authorization.Authorize]
8
- @inject ApiClientService ApiClient
9
- @inject IJSRuntime JS
10
- @inject AuthenticationStateProvider AuthStateProvider
11
-
12
- <PageTitle>Widget Setup</PageTitle>
13
-
14
- <SuccessAlert Message="@successMessage" />
15
-
16
- <div class="space-y-6">
17
- <div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
18
- <div>
19
- <p class="text-sm font-semibold uppercase tracking-wider text-violet-600">Dashboard Admin</p>
20
- <h2 class="mt-1 text-3xl font-bold tracking-tight text-slate-900">Widget Setup</h2>
21
- <p class="mt-1 text-slate-500">Manage the widget catalog and decide which roles can use each widget.</p>
22
- </div>
23
- @if (isAdmin)
24
- {
25
- <button @onclick="OpenCreateModal" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-violet-700">
26
- <i data-lucide="plus" class="mr-2 h-4 w-4"></i>
27
- New Widget
28
- </button>
29
- }
30
- </div>
31
-
32
- @if (isLoading)
33
- {
34
- <LoadingSpinner Message="Loading widgets..." />
35
- }
36
- else if (!isAdmin)
37
- {
38
- <div class="rounded-2xl border border-rose-100 bg-rose-50 p-6 text-rose-700">
39
- You do not have permission to manage dashboard widgets.
40
- </div>
41
- }
42
- else
43
- {
44
- <div class="grid gap-6 xl:grid-cols-[360px_minmax(0,1fr)]">
45
- <div class="rounded-2xl border border-slate-200 bg-white shadow-sm overflow-hidden">
46
- <div class="border-b border-slate-100 bg-slate-50/70 p-4">
47
- <div class="flex items-center justify-between gap-3 mb-2">
48
- <label class="block text-xs font-semibold uppercase tracking-wider text-slate-500">Search widgets</label>
49
- <div class="flex items-center gap-2">
50
- @if (hasPendingOrderChanges)
51
- {
52
- <span class="rounded-full bg-amber-50 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wider text-amber-700">Order changed</span>
53
- }
54
- <button @onclick="SaveWidgetOrderAsync" disabled="@(!hasPendingOrderChanges || isSavingWidgetOrder)"
55
- class="rounded-lg bg-violet-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-violet-700 disabled:opacity-50">
56
- @(isSavingWidgetOrder ? "Saving order..." : "Save order")
57
- </button>
58
- </div>
59
- </div>
60
- <input @bind-value="searchInput" @bind-value:event="oninput" type="text" placeholder="Search by code, name, or category"
61
- class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent" />
62
- </div>
63
- <div class="max-h-[70vh] overflow-y-auto p-3">
64
- @if (FilteredWidgets.Any())
65
- {
66
- @foreach (var widget in FilteredWidgets)
67
- {
68
- var isSelected = selectedWidget?.WidgetId == widget.WidgetId;
69
- <button @onclick="() => SelectWidget(widget)"
70
- draggable="true"
71
- @ondragstart="() => BeginWidgetDrag(widget.WidgetId)"
72
- @ondrop="() => DropWidget(widget.WidgetId)"
73
- @ondragover:preventDefault="true"
74
- class="mb-2 w-full rounded-xl border px-4 py-3 text-left transition-all @(isSelected ? "border-violet-300 bg-violet-50 shadow-sm" : "border-slate-200 bg-white hover:bg-slate-50") @(draggedWidgetId == widget.WidgetId ? "ring-2 ring-violet-300" : "")">
75
- <div class="flex items-start justify-between gap-3">
76
- <div class="min-w-0">
77
- <p class="truncate text-sm font-semibold text-slate-900">@widget.WidgetName</p>
78
- <p class="mt-0.5 truncate text-xs text-slate-500">@widget.WidgetCode</p>
79
- </div>
80
- <span class="rounded-full bg-slate-100 px-2 py-1 text-[10px] font-semibold text-slate-500">@widget.RoleCount roles</span>
81
- </div>
82
- <div class="mt-2 flex flex-wrap gap-2 text-[10px] font-semibold uppercase tracking-wider">
83
- <span class="rounded-full bg-white px-2 py-1 text-slate-500 border border-slate-200">@DisplayCategory(widget.Category)</span>
84
- <span class="rounded-full @(widget.IsActive ? "bg-emerald-50 text-emerald-700" : "bg-slate-100 text-slate-500") px-2 py-1">@((widget.IsActive) ? "Active" : "Hidden")</span>
85
- </div>
86
- <div class="mt-2 flex items-center justify-between text-[10px] text-slate-400">
87
- <span>Drag to reorder</span>
88
- <span>Order @widget.DefaultOrder</span>
89
- </div>
90
- </button>
91
- }
92
- }
93
- else
94
- {
95
- <div class="rounded-xl border border-dashed border-slate-200 p-6 text-center text-sm text-slate-500">
96
- No widgets found.
97
- </div>
98
- }
99
- </div>
100
- </div>
101
-
102
- <div class="space-y-6">
103
- @if (selectedWidget != null)
104
- {
105
- <div class="rounded-2xl border border-slate-200 bg-white shadow-sm">
106
- <div class="flex flex-col gap-3 border-b border-slate-100 bg-slate-50/70 px-5 py-4 md:flex-row md:items-center md:justify-between">
107
- <div>
108
- <p class="text-xs font-semibold uppercase tracking-wider text-violet-600">Selected Widget</p>
109
- <h3 class="mt-1 text-xl font-bold text-slate-900">@selectedWidget.WidgetName</h3>
110
- <p class="text-sm text-slate-500">@selectedWidget.WidgetCode</p>
111
- </div>
112
- <div class="flex flex-wrap gap-2">
113
- <button @onclick="OpenEditModal" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">
114
- <i data-lucide="pencil" class="mr-2 h-4 w-4"></i>Edit
115
- </button>
116
- <button @onclick="RequestDeleteSelectedWidget" class="inline-flex items-center rounded-lg border border-rose-200 bg-rose-50 px-3 py-2 text-sm font-medium text-rose-700 hover:bg-rose-100">
117
- <i data-lucide="trash-2" class="mr-2 h-4 w-4"></i>Delete
118
- </button>
119
- </div>
120
- </div>
121
-
122
- <div class="grid gap-4 p-5 md:grid-cols-2 xl:grid-cols-4">
123
- <div class="rounded-xl bg-slate-50 p-4">
124
- <p class="text-[10px] font-semibold uppercase tracking-wider text-slate-400">Component</p>
125
- <p class="mt-1 text-sm font-semibold text-slate-900">@selectedWidget.ComponentKey</p>
126
- </div>
127
- <div class="rounded-xl bg-slate-50 p-4">
128
- <p class="text-[10px] font-semibold uppercase tracking-wider text-slate-400">Category</p>
129
- <p class="mt-1 text-sm font-semibold text-slate-900">@DisplayCategory(selectedWidget.Category)</p>
130
- </div>
131
- <div class="rounded-xl bg-slate-50 p-4">
132
- <p class="text-[10px] font-semibold uppercase tracking-wider text-slate-400">Default Size</p>
133
- <p class="mt-1 text-sm font-semibold text-slate-900">@selectedWidget.DefaultWidth x @selectedWidget.DefaultHeight</p>
134
- </div>
135
- <div class="rounded-xl bg-slate-50 p-4">
136
- <p class="text-[10px] font-semibold uppercase tracking-wider text-slate-400">Roles</p>
137
- <p class="mt-1 text-sm font-semibold text-slate-900">@selectedWidget.RoleCount</p>
138
- </div>
139
- </div>
140
- </div>
141
-
142
- <div class="rounded-2xl border border-slate-200 bg-white shadow-sm">
143
- <div class="flex items-center justify-between border-b border-slate-100 px-5 py-4">
144
- <div>
145
- <p class="text-xs font-semibold uppercase tracking-wider text-violet-600">Role Access</p>
146
- <h3 class="mt-1 text-lg font-bold text-slate-900">Who can use this widget</h3>
147
- </div>
148
- <button @onclick="SaveAccessAsync" disabled="@isSavingAccess"
149
- class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-violet-700 disabled:opacity-50">
150
- <i data-lucide="save" class="mr-2 h-4 w-4"></i>
151
- @(isSavingAccess ? "Saving..." : "Save Access")
152
- </button>
153
- </div>
154
-
155
- <div class="border-b border-slate-100 bg-slate-50/60 px-5 py-4">
156
- <div class="flex flex-wrap items-center gap-2">
157
- <span class="text-xs font-semibold uppercase tracking-wider text-slate-500">Presets</span>
158
- <button @onclick="ApplyPresetFullAccess" class="rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50">Full access</button>
159
- <button @onclick="ApplyPresetTeamDefault" class="rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50">Team default</button>
160
- <button @onclick="ApplyPresetClear" class="rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50">Clear all</button>
161
- </div>
162
- </div>
163
-
164
- <div class="overflow-x-auto">
165
- <table class="w-full">
166
- <thead>
167
- <tr class="border-b border-slate-100 bg-slate-50/60 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">
168
- <th class="px-5 py-3">Role</th>
169
- <th class="px-5 py-3">View</th>
170
- <th class="px-5 py-3">Configure</th>
171
- <th class="px-5 py-3">Default Visible</th>
172
- <th class="px-5 py-3">Grid X</th>
173
- <th class="px-5 py-3">Grid Y</th>
174
- <th class="px-5 py-3">Width</th>
175
- <th class="px-5 py-3">Height</th>
176
- <th class="px-5 py-3">Sort</th>
177
- </tr>
178
- </thead>
179
- <tbody class="divide-y divide-slate-100">
180
- @foreach (var access in selectedWidgetAccess)
181
- {
182
- <tr class="hover:bg-slate-50/50">
183
- <td class="px-5 py-4">
184
- <div>
185
- <p class="text-sm font-semibold text-slate-900">@access.RoleName</p>
186
- <p class="text-xs text-slate-400">Role #@access.RoleId</p>
187
- </div>
188
- </td>
189
- <td class="px-5 py-4">
190
- <input type="checkbox" class="h-4 w-4 rounded accent-violet-600" @bind="access.CanView" />
191
- </td>
192
- <td class="px-5 py-4">
193
- <input type="checkbox" class="h-4 w-4 rounded accent-violet-600" @bind="access.CanConfigure" />
194
- </td>
195
- <td class="px-5 py-4">
196
- <input type="checkbox" class="h-4 w-4 rounded accent-violet-600" @bind="access.IsDefaultVisible" />
197
- </td>
198
- <td class="px-5 py-4">
199
- <input type="number" class="w-20 rounded-lg border border-slate-200 px-2 py-1.5 text-sm" @bind="access.DefaultGridX" />
200
- </td>
201
- <td class="px-5 py-4">
202
- <input type="number" class="w-20 rounded-lg border border-slate-200 px-2 py-1.5 text-sm" @bind="access.DefaultGridY" />
203
- </td>
204
- <td class="px-5 py-4">
205
- <input type="number" class="w-20 rounded-lg border border-slate-200 px-2 py-1.5 text-sm" @bind="access.DefaultWidth" />
206
- </td>
207
- <td class="px-5 py-4">
208
- <input type="number" class="w-20 rounded-lg border border-slate-200 px-2 py-1.5 text-sm" @bind="access.DefaultHeight" />
209
- </td>
210
- <td class="px-5 py-4">
211
- <input type="number" class="w-20 rounded-lg border border-slate-200 px-2 py-1.5 text-sm" @bind="access.DefaultSortOrder" />
212
- </td>
213
- </tr>
214
- }
215
- </tbody>
216
- </table>
217
- </div>
218
-
219
- @if (!string.IsNullOrWhiteSpace(accessErrorMessage))
220
- {
221
- <div class="border-t border-slate-100 p-4">
222
- <div class="rounded-xl border border-rose-100 bg-rose-50 px-4 py-3 text-sm text-rose-700">@accessErrorMessage</div>
223
- </div>
224
- }
225
- </div>
226
- }
227
- else
228
- {
229
- <div class="rounded-2xl border border-dashed border-slate-200 bg-white p-10 text-center text-slate-500 shadow-sm">
230
- Select a widget to manage access.
231
- </div>
232
- }
233
- </div>
234
- </div>
235
- }
236
- </div>
237
-
238
- @if (showWidgetModal)
239
- {
240
- <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
241
- <div class="mx-4 w-full max-w-2xl overflow-hidden rounded-2xl bg-white shadow-2xl">
242
- <div class="flex items-center justify-between border-b border-slate-100 px-6 py-4">
243
- <div>
244
- <h3 class="text-lg font-semibold text-slate-900">@(isEditingWidget ? "Edit Widget" : "New Widget")</h3>
245
- <p class="text-sm text-slate-500">Define the widget catalog entry here.</p>
246
- </div>
247
- <button @onclick="CloseWidgetModal" class="rounded-lg p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-600">
248
- <i data-lucide="x" class="h-5 w-5"></i>
249
- </button>
250
- </div>
251
- <div class="space-y-4 p-6">
252
- <div class="grid gap-4 md:grid-cols-2">
253
- <div>
254
- <label class="mb-1.5 block text-sm font-medium text-slate-700">Widget Code</label>
255
- <input @bind="formWidgetCode" type="text" placeholder="WIDGET_TASKS"
256
- class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-violet-600" />
257
- </div>
258
- <div>
259
- <label class="mb-1.5 block text-sm font-medium text-slate-700">Widget Name</label>
260
- <input @bind="formWidgetName" type="text" placeholder="Task Overview"
261
- class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-violet-600" />
262
- </div>
263
- </div>
264
- <div>
265
- <label class="mb-1.5 block text-sm font-medium text-slate-700">Description</label>
266
- <textarea @bind="formDescription" rows="3" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-violet-600"></textarea>
267
- </div>
268
- <div class="grid gap-4 md:grid-cols-2">
269
- <div>
270
- <label class="mb-1.5 block text-sm font-medium text-slate-700">Category</label>
271
- <input @bind="formCategory" type="text" placeholder="Overview"
272
- class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-violet-600" />
273
- </div>
274
- <div>
275
- <label class="mb-1.5 block text-sm font-medium text-slate-700">Component Key</label>
276
- <input @bind="formComponentKey" type="text" placeholder="GreetingBanner"
277
- class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-violet-600" />
278
- </div>
279
- </div>
280
- <div class="grid gap-4 md:grid-cols-4">
281
- <div>
282
- <label class="mb-1.5 block text-sm font-medium text-slate-700">Data Source</label>
283
- <input @bind="formDataSourceKey" type="text" placeholder="DashboardSummary"
284
- class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-violet-600" />
285
- </div>
286
- <div>
287
- <label class="mb-1.5 block text-sm font-medium text-slate-700">Width</label>
288
- <input @bind="formDefaultWidth" type="number" min="1" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-violet-600" />
289
- </div>
290
- <div>
291
- <label class="mb-1.5 block text-sm font-medium text-slate-700">Height</label>
292
- <input @bind="formDefaultHeight" type="number" min="1" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-violet-600" />
293
- </div>
294
- <div>
295
- <label class="mb-1.5 block text-sm font-medium text-slate-700">Order</label>
296
- <input @bind="formDefaultOrder" type="number" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-violet-600" />
297
- </div>
298
- </div>
299
- <div class="flex items-center gap-2">
300
- <input @bind="formIsActive" type="checkbox" class="h-4 w-4 rounded accent-violet-600" id="widgetActiveToggle" />
301
- <label for="widgetActiveToggle" class="text-sm font-medium text-slate-700">Active</label>
302
- </div>
303
- @if (!string.IsNullOrWhiteSpace(widgetErrorMessage))
304
- {
305
- <div class="rounded-xl border border-rose-100 bg-rose-50 px-4 py-3 text-sm text-rose-700">@widgetErrorMessage</div>
306
- }
307
- </div>
308
- <div class="flex items-center justify-end gap-3 border-t border-slate-100 bg-slate-50/60 px-6 py-4">
309
- <button @onclick="CloseWidgetModal" class="rounded-lg px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100">Cancel</button>
310
- <button @onclick="SaveWidgetAsync" disabled="@isSavingWidget"
311
- class="rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-violet-700 disabled:opacity-50">
312
- @(isSavingWidget ? "Saving..." : "Save Widget")
313
- </button>
314
- </div>
315
- </div>
316
- </div>
317
- }
318
-
319
- <ConfirmDialog IsVisible="@showConfirmDialog"
320
- Title="@confirmTitle"
321
- Message="@confirmMessage"
322
- ConfirmText="@confirmButtonText"
323
- Icon="@confirmIcon"
324
- IconBgClass="@confirmIconBg"
325
- IconTextClass="@confirmIconText"
326
- ButtonClass="@confirmButtonClass"
327
- OnConfirm="HandleConfirmAction"
328
- OnCancel="CloseConfirmDialog" />
329
-
330
- @code {
331
- private bool isLoading = true;
332
- private bool isAdmin;
333
- private bool showWidgetModal;
334
- private bool isEditingWidget;
335
- private bool isSavingWidget;
336
- private bool isSavingAccess;
337
- private bool isSavingWidgetOrder;
338
- private bool hasPendingOrderChanges;
339
- private bool showConfirmDialog;
340
- private string? successMessage;
341
- private string? widgetErrorMessage;
342
- private string? accessErrorMessage;
343
- private string searchInput = string.Empty;
344
- private List<DashboardWidgetAdminDto> widgets = new();
345
- private DashboardWidgetAdminDto? selectedWidget;
346
- private List<DashboardWidgetRoleAccessDto> selectedWidgetAccess = new();
347
- private long? draggedWidgetId;
348
-
349
- private long editingWidgetId;
350
- private string formWidgetCode = string.Empty;
351
- private string formWidgetName = string.Empty;
352
- private string? formDescription;
353
- private string? formCategory;
354
- private string? formComponentKey;
355
- private string? formDataSourceKey;
356
- private int formDefaultWidth = 4;
357
- private int formDefaultHeight = 3;
358
- private int formDefaultOrder;
359
- private bool formIsActive = true;
360
-
361
- private string confirmTitle = string.Empty;
362
- private string confirmMessage = string.Empty;
363
- private string confirmButtonText = string.Empty;
364
- private string confirmIcon = string.Empty;
365
- private string confirmIconBg = string.Empty;
366
- private string confirmIconText = string.Empty;
367
- private string confirmButtonClass = string.Empty;
368
- private Func<Task>? confirmAction;
369
-
370
- protected override async Task OnInitializedAsync()
371
- {
372
- var authState = await AuthStateProvider.GetAuthenticationStateAsync();
373
- var user = authState.User;
374
- isAdmin = user.IsInRole("Admin");
375
-
376
- if (!isAdmin)
377
- {
378
- isLoading = false;
379
- return;
380
- }
381
-
382
- await LoadWidgetsAsync();
383
- if (widgets.Any())
384
- {
385
- await SelectWidget(widgets.First());
386
- }
387
-
388
- isLoading = false;
389
- }
390
-
391
- protected override async Task OnAfterRenderAsync(bool firstRender)
392
- {
393
- await JS.InvokeVoidAsync("initIcons");
394
- }
395
-
396
- private IEnumerable<DashboardWidgetAdminDto> FilteredWidgets =>
397
- string.IsNullOrWhiteSpace(searchInput)
398
- ? widgets
399
- : widgets.Where(widget =>
400
- widget.WidgetCode.Contains(searchInput, StringComparison.OrdinalIgnoreCase) ||
401
- widget.WidgetName.Contains(searchInput, StringComparison.OrdinalIgnoreCase) ||
402
- (widget.Category != null && widget.Category.Contains(searchInput, StringComparison.OrdinalIgnoreCase)));
403
-
404
- private async Task LoadWidgetsAsync()
405
- {
406
- var client = ApiClient.CreateClient();
407
- var result = await client.GetFromJsonAsync<Result<List<DashboardWidgetAdminDto>>>("Dashboard/admin/widgets", Serialization.CaseInsensitive);
408
- widgets = result?.Value ?? new();
409
- hasPendingOrderChanges = false;
410
- }
411
-
412
- private async Task SelectWidget(DashboardWidgetAdminDto widget)
413
- {
414
- selectedWidget = widget;
415
- accessErrorMessage = null;
416
- var client = ApiClient.CreateClient();
417
- var result = await client.GetFromJsonAsync<Result<List<DashboardWidgetRoleAccessDto>>>($"Dashboard/admin/widgets/{widget.WidgetId}/access", Serialization.CaseInsensitive);
418
- selectedWidgetAccess = result?.Value ?? new();
419
- }
420
-
421
- private void OpenCreateModal()
422
- {
423
- isEditingWidget = false;
424
- editingWidgetId = 0;
425
- formWidgetCode = string.Empty;
426
- formWidgetName = string.Empty;
427
- formDescription = null;
428
- formCategory = null;
429
- formComponentKey = null;
430
- formDataSourceKey = null;
431
- formDefaultWidth = 4;
432
- formDefaultHeight = 3;
433
- formDefaultOrder = widgets.Count > 0 ? widgets.Max(widget => widget.DefaultOrder) + 1 : 1;
434
- formIsActive = true;
435
- widgetErrorMessage = null;
436
- showWidgetModal = true;
437
- }
438
-
439
- private void OpenEditModal()
440
- {
441
- if (selectedWidget == null)
442
- {
443
- return;
444
- }
445
-
446
- isEditingWidget = true;
447
- editingWidgetId = selectedWidget.WidgetId;
448
- formWidgetCode = selectedWidget.WidgetCode;
449
- formWidgetName = selectedWidget.WidgetName;
450
- formDescription = selectedWidget.Description;
451
- formCategory = selectedWidget.Category;
452
- formComponentKey = selectedWidget.ComponentKey;
453
- formDataSourceKey = selectedWidget.DataSourceKey;
454
- formDefaultWidth = selectedWidget.DefaultWidth;
455
- formDefaultHeight = selectedWidget.DefaultHeight;
456
- formDefaultOrder = selectedWidget.DefaultOrder;
457
- formIsActive = selectedWidget.IsActive;
458
- widgetErrorMessage = null;
459
- showWidgetModal = true;
460
- }
461
-
462
- private void CloseWidgetModal()
463
- {
464
- showWidgetModal = false;
465
- widgetErrorMessage = null;
466
- }
467
-
468
- private async Task SaveWidgetAsync()
469
- {
470
- widgetErrorMessage = null;
471
-
472
- if (string.IsNullOrWhiteSpace(formWidgetCode))
473
- {
474
- widgetErrorMessage = "Widget code is required.";
475
- return;
476
- }
477
-
478
- if (string.IsNullOrWhiteSpace(formWidgetName))
479
- {
480
- widgetErrorMessage = "Widget name is required.";
481
- return;
482
- }
483
-
484
- isSavingWidget = true;
485
- var client = ApiClient.CreateClient();
486
- try
487
- {
488
- var dto = new DashboardWidgetUpsertDto
489
- {
490
- WidgetId = editingWidgetId,
491
- WidgetCode = formWidgetCode.Trim(),
492
- WidgetName = formWidgetName.Trim(),
493
- Description = string.IsNullOrWhiteSpace(formDescription) ? null : formDescription.Trim(),
494
- Category = string.IsNullOrWhiteSpace(formCategory) ? null : formCategory.Trim(),
495
- ComponentKey = string.IsNullOrWhiteSpace(formComponentKey) ? null : formComponentKey.Trim(),
496
- DataSourceKey = string.IsNullOrWhiteSpace(formDataSourceKey) ? null : formDataSourceKey.Trim(),
497
- DefaultWidth = formDefaultWidth,
498
- DefaultHeight = formDefaultHeight,
499
- DefaultOrder = formDefaultOrder,
500
- IsActive = formIsActive
501
- };
502
-
503
- HttpResponseMessage response;
504
- if (isEditingWidget)
505
- {
506
- response = await client.PutAsJsonAsync($"Dashboard/admin/widgets/{editingWidgetId}", dto);
507
- }
508
- else
509
- {
510
- response = await client.PostAsJsonAsync("Dashboard/admin/widgets", dto);
511
- }
512
-
513
- var result = await response.Content.ReadFromJsonAsync<Result<DashboardWidgetAdminDto>>(Serialization.CaseInsensitive);
514
- if (result == null || !result.IsSuccess)
515
- {
516
- widgetErrorMessage = result?.ErrorMessage ?? "Failed to save widget.";
517
- return;
518
- }
519
-
520
- await LoadWidgetsAsync();
521
- selectedWidget = widgets.FirstOrDefault(widget => widget.WidgetId == result.Value?.WidgetId) ?? widgets.FirstOrDefault();
522
- if (selectedWidget != null)
523
- {
524
- await SelectWidget(selectedWidget);
525
- }
526
-
527
- CloseWidgetModal();
528
- successMessage = isEditingWidget ? "Widget updated successfully." : "Widget created successfully.";
529
- }
530
- catch (Exception ex)
531
- {
532
- widgetErrorMessage = ex.Message;
533
- }
534
- finally
535
- {
536
- isSavingWidget = false;
537
- }
538
- }
539
-
540
- private async Task SaveAccessAsync()
541
- {
542
- if (selectedWidget == null)
543
- {
544
- return;
545
- }
546
-
547
- isSavingAccess = true;
548
- accessErrorMessage = null;
549
- try
550
- {
551
- var client = ApiClient.CreateClient();
552
- var request = new DashboardWidgetRoleAccessSaveRequestDto
553
- {
554
- RoleAccess = selectedWidgetAccess
555
- };
556
-
557
- var response = await client.PutAsJsonAsync($"Dashboard/admin/widgets/{selectedWidget.WidgetId}/access", request);
558
- var result = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
559
- if (result == null || !result.IsSuccess)
560
- {
561
- accessErrorMessage = result?.ErrorMessage ?? "Failed to save widget access.";
562
- return;
563
- }
564
-
565
- successMessage = $"Access saved for {selectedWidget.WidgetName}.";
566
- await LoadWidgetsAsync();
567
- selectedWidget = widgets.FirstOrDefault(widget => widget.WidgetId == selectedWidget.WidgetId) ?? selectedWidget;
568
- }
569
- catch (Exception ex)
570
- {
571
- accessErrorMessage = ex.Message;
572
- }
573
- finally
574
- {
575
- isSavingAccess = false;
576
- }
577
- }
578
-
579
- private void BeginWidgetDrag(long widgetId)
580
- {
581
- draggedWidgetId = widgetId;
582
- }
583
-
584
- private void DropWidget(long targetWidgetId)
585
- {
586
- if (draggedWidgetId == null || draggedWidgetId == targetWidgetId)
587
- {
588
- draggedWidgetId = null;
589
- return;
590
- }
591
-
592
- var fromIndex = widgets.FindIndex(widget => widget.WidgetId == draggedWidgetId.Value);
593
- var toIndex = widgets.FindIndex(widget => widget.WidgetId == targetWidgetId);
594
- if (fromIndex < 0 || toIndex < 0)
595
- {
596
- draggedWidgetId = null;
597
- return;
598
- }
599
-
600
- var moved = widgets[fromIndex];
601
- widgets.RemoveAt(fromIndex);
602
- if (fromIndex < toIndex)
603
- {
604
- toIndex--;
605
- }
606
- widgets.Insert(toIndex, moved);
607
- hasPendingOrderChanges = true;
608
- draggedWidgetId = null;
609
- }
610
-
611
- private async Task SaveWidgetOrderAsync()
612
- {
613
- if (!hasPendingOrderChanges || isSavingWidgetOrder)
614
- {
615
- return;
616
- }
617
-
618
- isSavingWidgetOrder = true;
619
- widgetErrorMessage = null;
620
-
621
- try
622
- {
623
- var client = ApiClient.CreateClient();
624
- for (var index = 0; index < widgets.Count; index++)
625
- {
626
- var widget = widgets[index];
627
- var dto = new DashboardWidgetUpsertDto
628
- {
629
- WidgetId = widget.WidgetId,
630
- WidgetCode = widget.WidgetCode,
631
- WidgetName = widget.WidgetName,
632
- Description = widget.Description,
633
- Category = widget.Category,
634
- ComponentKey = widget.ComponentKey,
635
- DataSourceKey = widget.DataSourceKey,
636
- DefaultWidth = widget.DefaultWidth,
637
- DefaultHeight = widget.DefaultHeight,
638
- DefaultOrder = index + 1,
639
- IsActive = widget.IsActive
640
- };
641
-
642
- var response = await client.PutAsJsonAsync($"Dashboard/admin/widgets/{widget.WidgetId}", dto);
643
- var result = await response.Content.ReadFromJsonAsync<Result<DashboardWidgetAdminDto>>(Serialization.CaseInsensitive);
644
- if (result == null || !result.IsSuccess)
645
- {
646
- widgetErrorMessage = result?.ErrorMessage ?? "Failed to save widget order.";
647
- return;
648
- }
649
- }
650
-
651
- await LoadWidgetsAsync();
652
- if (selectedWidget != null)
653
- {
654
- selectedWidget = widgets.FirstOrDefault(widget => widget.WidgetId == selectedWidget.WidgetId);
655
- }
656
- hasPendingOrderChanges = false;
657
- successMessage = "Widget order saved.";
658
- }
659
- catch (Exception ex)
660
- {
661
- widgetErrorMessage = ex.Message;
662
- }
663
- finally
664
- {
665
- isSavingWidgetOrder = false;
666
- }
667
- }
668
-
669
- private void ApplyPresetFullAccess()
670
- {
671
- if (selectedWidget == null)
672
- {
673
- return;
674
- }
675
-
676
- foreach (var access in selectedWidgetAccess)
677
- {
678
- access.CanView = true;
679
- access.CanConfigure = access.RoleName.Equals("Admin", StringComparison.OrdinalIgnoreCase);
680
- access.IsDefaultVisible = true;
681
- access.DefaultGridX = 0;
682
- access.DefaultGridY = 0;
683
- access.DefaultWidth = selectedWidget.DefaultWidth;
684
- access.DefaultHeight = selectedWidget.DefaultHeight;
685
- access.DefaultSortOrder = selectedWidget.DefaultOrder;
686
- }
687
- }
688
-
689
- private void ApplyPresetTeamDefault()
690
- {
691
- if (selectedWidget == null)
692
- {
693
- return;
694
- }
695
-
696
- var adminOnlyWidget = selectedWidget.Category != null &&
697
- selectedWidget.Category.Contains("Admin", StringComparison.OrdinalIgnoreCase);
698
-
699
- foreach (var access in selectedWidgetAccess)
700
- {
701
- var isAdmin = access.RoleName.Equals("Admin", StringComparison.OrdinalIgnoreCase);
702
- var isManager = access.RoleName.Equals("Manager", StringComparison.OrdinalIgnoreCase);
703
- var canView = isAdmin || isManager || !adminOnlyWidget;
704
-
705
- access.CanView = canView;
706
- access.CanConfigure = isAdmin;
707
- access.IsDefaultVisible = canView;
708
- access.DefaultGridX = 0;
709
- access.DefaultGridY = 0;
710
- access.DefaultWidth = selectedWidget.DefaultWidth;
711
- access.DefaultHeight = selectedWidget.DefaultHeight;
712
- access.DefaultSortOrder = selectedWidget.DefaultOrder;
713
- }
714
- }
715
-
716
- private void ApplyPresetClear()
717
- {
718
- if (selectedWidget == null)
719
- {
720
- return;
721
- }
722
-
723
- foreach (var access in selectedWidgetAccess)
724
- {
725
- access.CanView = false;
726
- access.CanConfigure = false;
727
- access.IsDefaultVisible = false;
728
- access.DefaultGridX = 0;
729
- access.DefaultGridY = 0;
730
- access.DefaultWidth = selectedWidget.DefaultWidth;
731
- access.DefaultHeight = selectedWidget.DefaultHeight;
732
- access.DefaultSortOrder = selectedWidget.DefaultOrder;
733
- }
734
- }
735
-
736
- private void RequestDeleteSelectedWidget()
737
- {
738
- if (selectedWidget == null)
739
- {
740
- return;
741
- }
742
-
743
- confirmTitle = "Delete widget?";
744
- confirmMessage = $"This will hide '{selectedWidget.WidgetName}' from the catalog and remove its role access.";
745
- confirmButtonText = "Delete";
746
- confirmIcon = "trash-2";
747
- confirmIconBg = "bg-rose-50";
748
- confirmIconText = "text-rose-600";
749
- confirmButtonClass = "bg-rose-600 hover:bg-rose-700";
750
- confirmAction = DeleteSelectedWidgetAsync;
751
- showConfirmDialog = true;
752
- }
753
-
754
- private async Task DeleteSelectedWidgetAsync()
755
- {
756
- if (selectedWidget == null)
757
- {
758
- return;
759
- }
760
-
761
- var client = ApiClient.CreateClient();
762
- var response = await client.DeleteAsync($"Dashboard/admin/widgets/{selectedWidget.WidgetId}");
763
- var result = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
764
- if (result == null || !result.IsSuccess)
765
- {
766
- accessErrorMessage = result?.ErrorMessage ?? "Failed to delete widget.";
767
- return;
768
- }
769
-
770
- await LoadWidgetsAsync();
771
- selectedWidget = widgets.FirstOrDefault();
772
- if (selectedWidget != null)
773
- {
774
- await SelectWidget(selectedWidget);
775
- }
776
- else
777
- {
778
- selectedWidgetAccess = new();
779
- }
780
-
781
- successMessage = "Widget deleted successfully.";
782
- }
783
-
784
- private async Task HandleConfirmAction()
785
- {
786
- showConfirmDialog = false;
787
- if (confirmAction != null)
788
- {
789
- await confirmAction();
790
- }
791
- }
792
-
793
- private void CloseConfirmDialog()
794
- {
795
- showConfirmDialog = false;
796
- }
797
-
798
- private static string DisplayCategory(string? category) =>
799
- string.IsNullOrWhiteSpace(category) ? "Uncategorized" : category;
800
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/reset-role-access-ids.sql DELETED
File without changes