Spaces:
Sleeping
Sleeping
User commited on
Commit ·
e40d850
1
Parent(s): 64a3c1d
fix: remove dashboard widgets flow
Browse files- TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs +0 -93
- TaskTrackingSystem.Database/AppDbContextModels/DashboardWidget.cs +0 -39
- TaskTrackingSystem.Database/AppDbContextModels/Role.cs +0 -2
- TaskTrackingSystem.Database/AppDbContextModels/RoleDashboardWidget.cs +0 -42
- TaskTrackingSystem.Database/AppDbContextModels/User.cs +0 -1
- TaskTrackingSystem.Database/AppDbContextModels/UserDashboardLayout.cs +0 -42
- TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetAdminDto.cs +0 -37
- TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetDto.cs +0 -49
- TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetLayoutItemDto.cs +0 -23
- TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetLayoutSaveRequestDto.cs +0 -9
- TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetRoleAccessDto.cs +0 -25
- TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetRoleAccessSaveRequestDto.cs +0 -9
- TaskTrackingSystem.Shared/Models/Dashboard/DashboardWidgetUpsertDto.cs +0 -35
- TaskTrackingSystem.WebApi/Features/Dashboard/DashboardController.cs +1 -100
- TaskTrackingSystem.WebApi/Features/Dashboard/DashboardService.cs +3 -528
- TaskTrackingSystem.WebApi/Program.cs +0 -141
- TaskTrackingSystem.WebApp/Components/Layout/MainLayout.razor +0 -1
- TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor +0 -4
- TaskTrackingSystem.WebApp/Components/Pages/Home.razor +334 -338
- TaskTrackingSystem.WebApp/Components/Pages/WidgetSetup.razor +0 -800
- 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
|
| 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);
|
| 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.
|
| 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>
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 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-
|
| 39 |
-
<h2 class="mt-
|
| 40 |
-
<p class="mt-
|
|
|
|
|
|
|
| 41 |
</div>
|
| 42 |
<div class="flex flex-wrap gap-3">
|
| 43 |
@if (canCreateTask)
|
| 44 |
{
|
| 45 |
-
<a href="/tasks" class="inline-flex items-center rounded-
|
| 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-
|
| 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 |
-
|
| 65 |
-
|
| 66 |
-
<div class="
|
| 67 |
-
<
|
|
|
|
|
|
|
|
|
|
| 68 |
</div>
|
| 69 |
-
|
|
|
|
|
|
|
| 70 |
|
| 71 |
-
<div class="rounded-
|
| 72 |
-
<div class="flex
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
<div>
|
| 74 |
-
<
|
| 75 |
-
<
|
| 76 |
-
<p class="mt-1 text-sm text-slate-500">Drag to reorder when editing is enabled.</p>
|
| 77 |
</div>
|
| 78 |
-
<div class="
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
{
|
| 85 |
-
<
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
}
|
| 90 |
</div>
|
| 91 |
</div>
|
|
|
|
| 92 |
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
{
|
| 99 |
-
<
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
{
|
| 255 |
-
<div
|
| 256 |
-
<
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 278 |
-
private
|
| 279 |
-
|
| 280 |
-
private
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 306 |
-
if (
|
| 307 |
{
|
| 308 |
-
|
| 309 |
}
|
| 310 |
|
| 311 |
-
var
|
| 312 |
-
if (
|
| 313 |
-
{
|
| 314 |
-
dashboardWidgets = widgetsResult.Value;
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
if (user.IsInRole("Admin"))
|
| 318 |
{
|
| 319 |
-
|
| 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
|
| 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
|
| 342 |
-
string.IsNullOrWhiteSpace(category) ? "General" : category;
|
| 343 |
|
| 344 |
-
private
|
| 345 |
{
|
| 346 |
-
var
|
| 347 |
-
|
| 348 |
-
|
| 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
|
| 360 |
-
return relevantUsers.Select(user =>
|
| 361 |
{
|
| 362 |
-
var
|
| 363 |
-
var
|
| 364 |
-
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
}
|
| 367 |
|
| 368 |
-
private string
|
| 369 |
{
|
| 370 |
-
|
| 371 |
{
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
|
| 376 |
-
|
|
|
|
| 377 |
|
| 378 |
-
|
| 379 |
{
|
| 380 |
-
|
| 381 |
}
|
| 382 |
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
}
|
| 392 |
|
| 393 |
-
|
| 394 |
-
{
|
| 395 |
-
if (!isWidgetEditMode)
|
| 396 |
{
|
| 397 |
-
return;
|
| 398 |
}
|
| 399 |
|
| 400 |
-
|
| 401 |
}
|
| 402 |
|
| 403 |
-
private
|
| 404 |
{
|
| 405 |
-
|
|
|
|
|
|
|
| 406 |
{
|
| 407 |
-
|
| 408 |
-
return;
|
| 409 |
}
|
| 410 |
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
|
| 419 |
-
var
|
| 420 |
-
dashboardWidgets.RemoveAt(fromIndex);
|
| 421 |
-
if (fromIndex < toIndex)
|
| 422 |
{
|
| 423 |
-
|
| 424 |
}
|
| 425 |
-
|
| 426 |
-
|
| 427 |
}
|
| 428 |
|
| 429 |
-
private
|
| 430 |
{
|
| 431 |
-
|
| 432 |
{
|
| 433 |
-
|
| 434 |
-
|
|
|
|
|
|
|
|
|
|
| 435 |
|
| 436 |
-
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
|
| 439 |
-
|
|
|
|
| 440 |
{
|
| 441 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
{
|
| 471 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
}
|
| 473 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
{
|
| 475 |
-
|
| 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
|