Spaces:
Running
Running
User commited on
Commit ·
3bd1468
1
Parent(s): 3418204
add: Timelog table, BudgetedHours, estimated hours
Browse files- README.md +5 -0
- TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs +94 -119
- TaskTrackingSystem.Database/AppDbContextModels/Comment.cs +1 -1
- TaskTrackingSystem.Database/AppDbContextModels/FileAttachment.cs +1 -1
- TaskTrackingSystem.Database/AppDbContextModels/MenuAdmin.cs +19 -21
- TaskTrackingSystem.Database/AppDbContextModels/MenuAdminDetail.cs +17 -20
- TaskTrackingSystem.Database/AppDbContextModels/Project.cs +3 -1
- TaskTrackingSystem.Database/AppDbContextModels/Role.cs +1 -3
- TaskTrackingSystem.Database/AppDbContextModels/RoleMenu.cs +18 -16
- TaskTrackingSystem.Database/AppDbContextModels/Task.cs +3 -1
- TaskTrackingSystem.Database/AppDbContextModels/TimeLog.cs +33 -0
- TaskTrackingSystem.Database/AppDbContextModels/User.cs +4 -2
- TaskTrackingSystem.Shared/Models/Project/CreateProjectDto.cs +2 -0
- TaskTrackingSystem.Shared/Models/Project/ProjectDto.cs +1 -0
- TaskTrackingSystem.Shared/Models/Project/UpdateProjectDto.cs +2 -0
- TaskTrackingSystem.Shared/Models/Report/TaskReportDto.cs +1 -1
- TaskTrackingSystem.Shared/Models/Report/TimeTrackingReportDto.cs +59 -0
- TaskTrackingSystem.Shared/Models/Report/UserProductivityDto.cs +1 -0
- TaskTrackingSystem.Shared/Models/Task/CreateTaskDto.cs +1 -1
- TaskTrackingSystem.Shared/Models/Task/TaskDto.cs +2 -1
- TaskTrackingSystem.Shared/Models/Task/UpdateTaskDto.cs +1 -1
- TaskTrackingSystem.WebApi/Features/Project/ProjectService.cs +17 -5
- TaskTrackingSystem.WebApi/Features/Report/ReportController.cs +12 -0
- TaskTrackingSystem.WebApi/Features/Report/ReportService.cs +165 -8
- TaskTrackingSystem.WebApi/Features/Role/RoleController.cs +0 -21
- TaskTrackingSystem.WebApi/Features/Role/RoleService.cs +0 -68
- TaskTrackingSystem.WebApi/Features/Task/TaskService.cs +23 -4
- TaskTrackingSystem.WebApp/Components/Pages/EmployeeReport.razor +22 -4
- TaskTrackingSystem.WebApp/Components/Pages/OverdueTasksReport.razor +120 -0
- TaskTrackingSystem.WebApp/Components/Pages/ProjectAssign.razor +10 -1
- TaskTrackingSystem.WebApp/Components/Pages/ProjectProgressReport.razor +72 -2
- TaskTrackingSystem.WebApp/Components/Pages/Projects.razor +11 -2
- TaskTrackingSystem.WebApp/Components/Pages/TaskAssign.razor +10 -1
- TaskTrackingSystem.WebApp/Components/Pages/TaskReport.razor +16 -5
- TaskTrackingSystem.WebApp/Components/Pages/Tasks.razor +1 -1
- TaskTrackingSystem.WebApp/Components/Pages/TimeTrackingReport.razor +135 -170
- TaskTrackingSystem.sln +7 -2
README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
dotnet tool install --global dotnet-ef
|
| 2 |
+
|
| 3 |
+
dotnet ef dbcontext scaffold "Server=.;Database=TTS;User Id=sa;Password=sasa@123;TrustServerCertificate=True;" Microsoft.EntityFrameworkCore.SqlServer --output-dir AppDbContextModels --context AppDbContext --force
|
| 4 |
+
|
| 5 |
+
|
TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs
CHANGED
|
@@ -21,7 +21,9 @@ public partial class AppDbContext : DbContext
|
|
| 21 |
|
| 22 |
public virtual DbSet<FileAttachment> FileAttachments { get; set; }
|
| 23 |
|
| 24 |
-
public virtual DbSet<
|
|
|
|
|
|
|
| 25 |
|
| 26 |
public virtual DbSet<Project> Projects { get; set; }
|
| 27 |
|
|
@@ -29,35 +31,25 @@ public partial class AppDbContext : DbContext
|
|
| 29 |
|
| 30 |
public virtual DbSet<Role> Roles { get; set; }
|
| 31 |
|
| 32 |
-
public virtual DbSet<
|
| 33 |
|
| 34 |
public virtual DbSet<Task> Tasks { get; set; }
|
| 35 |
|
| 36 |
public virtual DbSet<TaskHistory> TaskHistories { get; set; }
|
| 37 |
|
| 38 |
-
public virtual DbSet<
|
| 39 |
|
| 40 |
-
public virtual DbSet<
|
| 41 |
-
|
| 42 |
-
public virtual DbSet<MenuAdmin> MenuAdmins { get; set; }
|
| 43 |
-
public virtual DbSet<MenuAdminDetail> MenuAdminDetails { get; set; }
|
| 44 |
|
| 45 |
-
// protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
| 46 |
-
//#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
|
| 47 |
-
// => optionsBuilder.UseSqlServer("Server=.;Database=TTS;User Id=sa;Password=sasa@123;TrustServerCertificate=True;");
|
| 48 |
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
// This tells EF Core to look for a connection string named "DefaultConnection" in appsettings.json
|
| 53 |
-
optionsBuilder.UseSqlServer("Name=DefaultConnection");
|
| 54 |
-
}
|
| 55 |
-
}
|
| 56 |
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
| 57 |
{
|
| 58 |
modelBuilder.Entity<AuditLog>(entity =>
|
| 59 |
{
|
| 60 |
-
entity.
|
| 61 |
|
| 62 |
entity.Property(e => e.Action).HasMaxLength(100);
|
| 63 |
entity.Property(e => e.CreatedAt)
|
|
@@ -70,66 +62,75 @@ public partial class AppDbContext : DbContext
|
|
| 70 |
|
| 71 |
entity.HasOne(d => d.User).WithMany(p => p.AuditLogs)
|
| 72 |
.HasForeignKey(d => d.UserId)
|
| 73 |
-
.HasConstraintName("
|
| 74 |
});
|
| 75 |
|
| 76 |
modelBuilder.Entity<Comment>(entity =>
|
| 77 |
{
|
| 78 |
-
entity.
|
|
|
|
|
|
|
| 79 |
|
| 80 |
entity.Property(e => e.CreatedAt)
|
| 81 |
.HasDefaultValueSql("(getdate())")
|
| 82 |
.HasColumnType("datetime");
|
| 83 |
-
entity.Property(e => e.IsDeleted).HasDefaultValue(false);
|
| 84 |
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
| 85 |
|
| 86 |
entity.HasOne(d => d.Task).WithMany(p => p.Comments)
|
| 87 |
.HasForeignKey(d => d.TaskId)
|
| 88 |
-
.HasConstraintName("
|
| 89 |
|
| 90 |
entity.HasOne(d => d.User).WithMany(p => p.Comments)
|
| 91 |
.HasForeignKey(d => d.UserId)
|
| 92 |
.OnDelete(DeleteBehavior.ClientSetNull)
|
| 93 |
-
.HasConstraintName("
|
| 94 |
});
|
| 95 |
|
| 96 |
modelBuilder.Entity<FileAttachment>(entity =>
|
| 97 |
{
|
| 98 |
-
entity.
|
| 99 |
|
| 100 |
entity.Property(e => e.CreatedAt)
|
| 101 |
.HasDefaultValueSql("(getdate())")
|
| 102 |
.HasColumnType("datetime");
|
| 103 |
entity.Property(e => e.FileName).HasMaxLength(255);
|
| 104 |
entity.Property(e => e.FileType).HasMaxLength(100);
|
| 105 |
-
entity.Property(e => e.IsDeleted).HasDefaultValue(false);
|
| 106 |
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
| 107 |
|
| 108 |
entity.HasOne(d => d.Task).WithMany(p => p.FileAttachments)
|
| 109 |
.HasForeignKey(d => d.TaskId)
|
| 110 |
.OnDelete(DeleteBehavior.ClientSetNull)
|
| 111 |
-
.HasConstraintName("
|
| 112 |
});
|
| 113 |
|
| 114 |
-
modelBuilder.Entity<
|
| 115 |
{
|
| 116 |
-
entity.HasKey(e => e.
|
| 117 |
|
| 118 |
-
entity.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
entity.Property(e => e.
|
| 124 |
-
entity.Property(e => e.
|
| 125 |
-
entity.Property(e => e.
|
| 126 |
-
entity.Property(e => e.
|
| 127 |
-
entity.Property(e => e.
|
|
|
|
| 128 |
});
|
| 129 |
|
| 130 |
modelBuilder.Entity<Project>(entity =>
|
| 131 |
{
|
| 132 |
-
entity.
|
| 133 |
|
| 134 |
entity.Property(e => e.CreatedAt)
|
| 135 |
.HasDefaultValueSql("(getdate())")
|
|
@@ -138,20 +139,20 @@ public partial class AppDbContext : DbContext
|
|
| 138 |
.HasDefaultValueSql("(getdate())")
|
| 139 |
.HasColumnType("datetime");
|
| 140 |
entity.Property(e => e.EndDate).HasColumnType("datetime");
|
| 141 |
-
entity.Property(e => e.IsDeleted).HasDefaultValue(false);
|
| 142 |
entity.Property(e => e.Name).HasMaxLength(150);
|
| 143 |
entity.Property(e => e.StartDate).HasColumnType("datetime");
|
| 144 |
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
| 145 |
|
| 146 |
entity.HasOne(d => d.CreatedByNavigation).WithMany(p => p.Projects)
|
| 147 |
.HasForeignKey(d => d.CreatedById)
|
| 148 |
-
.OnDelete(DeleteBehavior.ClientSetNull)
|
| 149 |
-
.HasConstraintName("FK__Projects__Create__693CA210");
|
| 150 |
});
|
| 151 |
|
| 152 |
modelBuilder.Entity<ProjectMember>(entity =>
|
| 153 |
{
|
| 154 |
-
entity.HasKey(e => new { e.ProjectId, e.UserId })
|
|
|
|
|
|
|
| 155 |
|
| 156 |
entity.Property(e => e.CreatedAt)
|
| 157 |
.HasDefaultValueSql("(getdate())")
|
|
@@ -159,75 +160,64 @@ public partial class AppDbContext : DbContext
|
|
| 159 |
|
| 160 |
entity.HasOne(d => d.Project).WithMany(p => p.ProjectMembers)
|
| 161 |
.HasForeignKey(d => d.ProjectId)
|
| 162 |
-
.HasConstraintName("
|
| 163 |
|
| 164 |
entity.HasOne(d => d.User).WithMany(p => p.ProjectMembers)
|
| 165 |
.HasForeignKey(d => d.UserId)
|
| 166 |
-
.HasConstraintName("
|
| 167 |
});
|
| 168 |
|
| 169 |
modelBuilder.Entity<Role>(entity =>
|
| 170 |
{
|
| 171 |
-
entity.
|
| 172 |
-
|
| 173 |
-
entity.HasIndex(e => e.Name, "UQ__Roles__737584F64A9432D8").IsUnique();
|
| 174 |
|
| 175 |
entity.Property(e => e.CreatedAt)
|
| 176 |
.HasDefaultValueSql("(getdate())")
|
| 177 |
.HasColumnType("datetime");
|
| 178 |
entity.Property(e => e.Description).HasMaxLength(200);
|
| 179 |
-
entity.Property(e => e.IsDeleted).HasDefaultValue(false);
|
| 180 |
entity.Property(e => e.Name).HasMaxLength(50);
|
| 181 |
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
| 182 |
});
|
| 183 |
|
| 184 |
-
modelBuilder.Entity<
|
| 185 |
{
|
| 186 |
-
entity.
|
| 187 |
-
|
| 188 |
-
entity.Property(e => e.
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
entity.HasOne(d => d.Permission).WithMany(p => p.RolePermissions)
|
| 193 |
-
.HasForeignKey(d => d.PermissionId)
|
| 194 |
-
.HasConstraintName("FK__RolePermi__Permi__5EBF139D");
|
| 195 |
-
|
| 196 |
-
entity.HasOne(d => d.Role).WithMany(p => p.RolePermissions)
|
| 197 |
-
.HasForeignKey(d => d.RoleId)
|
| 198 |
-
.HasConstraintName("FK__RolePermi__RoleI__5DCAEF64");
|
| 199 |
});
|
| 200 |
|
| 201 |
modelBuilder.Entity<Task>(entity =>
|
| 202 |
{
|
| 203 |
-
entity.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
entity.Property(e => e.CreatedAt)
|
| 206 |
.HasDefaultValueSql("(getdate())")
|
| 207 |
.HasColumnType("datetime");
|
| 208 |
entity.Property(e => e.DueDate).HasColumnType("datetime");
|
| 209 |
-
entity.Property(e => e.
|
| 210 |
entity.Property(e => e.PriorityId).HasDefaultValue(2L);
|
| 211 |
entity.Property(e => e.StatusId).HasDefaultValue(1L);
|
| 212 |
entity.Property(e => e.Title).HasMaxLength(200);
|
| 213 |
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
| 214 |
|
| 215 |
-
entity.HasOne(d => d.AssignedByNavigation).WithMany(p => p.TaskAssignedByNavigations)
|
| 216 |
-
.HasForeignKey(d => d.AssignedBy)
|
| 217 |
-
.HasConstraintName("FK__Tasks__AssignedB__778AC167");
|
| 218 |
|
| 219 |
-
entity.HasOne(d => d.AssignedToNavigation).WithMany(p => p.TaskAssignedToNavigations)
|
| 220 |
-
.HasForeignKey(d => d.AssignedTo)
|
| 221 |
-
.HasConstraintName("FK__Tasks__AssignedT__76969D2E");
|
| 222 |
|
| 223 |
entity.HasOne(d => d.Project).WithMany(p => p.Tasks)
|
| 224 |
.HasForeignKey(d => d.ProjectId)
|
| 225 |
-
.HasConstraintName("
|
| 226 |
});
|
| 227 |
|
| 228 |
modelBuilder.Entity<TaskHistory>(entity =>
|
| 229 |
{
|
| 230 |
-
entity.
|
| 231 |
|
| 232 |
entity.Property(e => e.CreatedAt)
|
| 233 |
.HasDefaultValueSql("(getdate())")
|
|
@@ -236,26 +226,50 @@ public partial class AppDbContext : DbContext
|
|
| 236 |
|
| 237 |
entity.HasOne(d => d.CreatedByNavigation).WithMany(p => p.TaskHistoryCreatedByNavigations)
|
| 238 |
.HasForeignKey(d => d.CreatedBy)
|
| 239 |
-
.OnDelete(DeleteBehavior.ClientSetNull)
|
| 240 |
-
.HasConstraintName("FK__TaskHisto__Creat__0C85DE4D");
|
| 241 |
|
| 242 |
entity.HasOne(d => d.ModifiedBy).WithMany(p => p.TaskHistoryModifiedBies)
|
| 243 |
.HasForeignKey(d => d.ModifiedById)
|
| 244 |
.OnDelete(DeleteBehavior.ClientSetNull)
|
| 245 |
-
.HasConstraintName("
|
| 246 |
|
| 247 |
entity.HasOne(d => d.Task).WithMany(p => p.TaskHistories)
|
| 248 |
.HasForeignKey(d => d.TaskId)
|
| 249 |
-
.HasConstraintName("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
});
|
| 251 |
|
| 252 |
modelBuilder.Entity<User>(entity =>
|
| 253 |
{
|
| 254 |
-
entity.
|
| 255 |
|
| 256 |
-
entity.HasIndex(e => e.
|
| 257 |
|
| 258 |
-
entity.HasIndex(e => e.
|
| 259 |
|
| 260 |
entity.Property(e => e.CreatedAt)
|
| 261 |
.HasDefaultValueSql("(getdate())")
|
|
@@ -263,7 +277,6 @@ public partial class AppDbContext : DbContext
|
|
| 263 |
entity.Property(e => e.Email).HasMaxLength(256);
|
| 264 |
entity.Property(e => e.FirstName).HasMaxLength(50);
|
| 265 |
entity.Property(e => e.IsActive).HasDefaultValue(true);
|
| 266 |
-
entity.Property(e => e.IsDeleted).HasDefaultValue(false);
|
| 267 |
entity.Property(e => e.LastName).HasMaxLength(50);
|
| 268 |
entity.Property(e => e.Phone).HasMaxLength(20);
|
| 269 |
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
|
@@ -272,45 +285,7 @@ public partial class AppDbContext : DbContext
|
|
| 272 |
entity.HasOne(d => d.Role).WithMany(p => p.Users)
|
| 273 |
.HasForeignKey(d => d.RoleId)
|
| 274 |
.OnDelete(DeleteBehavior.ClientSetNull)
|
| 275 |
-
.HasConstraintName("
|
| 276 |
-
});
|
| 277 |
-
|
| 278 |
-
modelBuilder.Entity<MenuAdmin>(entity =>
|
| 279 |
-
{
|
| 280 |
-
entity.ToTable("AdminMenus");
|
| 281 |
-
entity.HasKey(e => e.AdminMenuId);
|
| 282 |
-
entity.Property(e => e.AdminMenuId).HasMaxLength(50);
|
| 283 |
-
entity.Property(e => e.MenuCode).HasMaxLength(50).IsRequired();
|
| 284 |
-
entity.Property(e => e.ParentCode).HasMaxLength(50).IsRequired();
|
| 285 |
-
entity.Property(e => e.MenuName).HasMaxLength(100).IsRequired();
|
| 286 |
-
entity.Property(e => e.MenuUrl).HasMaxLength(200);
|
| 287 |
-
entity.Property(e => e.Icon).HasMaxLength(50);
|
| 288 |
-
entity.Ignore(e => e.CreatedUserId);
|
| 289 |
-
entity.Ignore(e => e.CreatedDateTime);
|
| 290 |
-
entity.Ignore(e => e.ModifiedUserId);
|
| 291 |
-
entity.Ignore(e => e.ModifiedDateTime);
|
| 292 |
-
});
|
| 293 |
-
|
| 294 |
-
modelBuilder.Entity<MenuAdminDetail>(entity =>
|
| 295 |
-
{
|
| 296 |
-
entity.HasKey(e => e.MenuAdminDetailId);
|
| 297 |
-
entity.Property(e => e.MenuAdminDetailId).HasMaxLength(50);
|
| 298 |
-
entity.Property(e => e.MenuDetailCode).HasMaxLength(50).IsRequired();
|
| 299 |
-
entity.Property(e => e.ParentMenuCode).HasMaxLength(50).IsRequired();
|
| 300 |
-
entity.Property(e => e.ActionName).HasMaxLength(50).IsRequired();
|
| 301 |
-
entity.Property(e => e.ApiName).HasMaxLength(100).IsRequired();
|
| 302 |
-
entity.Property(e => e.CreatedUserId).HasMaxLength(50);
|
| 303 |
-
entity.Property(e => e.ModifiedUserId).HasMaxLength(50);
|
| 304 |
-
});
|
| 305 |
-
|
| 306 |
-
modelBuilder.Entity<RoleMenu>(entity =>
|
| 307 |
-
{
|
| 308 |
-
entity.HasKey(e => e.RoleMenuId);
|
| 309 |
-
entity.Property(e => e.RoleMenuId).HasMaxLength(50);
|
| 310 |
-
entity.Property(e => e.RoleCode).HasMaxLength(50).IsRequired();
|
| 311 |
-
entity.Property(e => e.MenuCode).HasMaxLength(50).IsRequired();
|
| 312 |
-
entity.Property(e => e.CreatedUserId).HasMaxLength(50);
|
| 313 |
-
entity.Property(e => e.ModifiedUserId).HasMaxLength(50);
|
| 314 |
});
|
| 315 |
|
| 316 |
OnModelCreatingPartial(modelBuilder);
|
|
|
|
| 21 |
|
| 22 |
public virtual DbSet<FileAttachment> FileAttachments { get; set; }
|
| 23 |
|
| 24 |
+
public virtual DbSet<MenuAdmin> MenuAdmins { get; set; }
|
| 25 |
+
|
| 26 |
+
public virtual DbSet<MenuAdminDetail> MenuAdminDetails { get; set; }
|
| 27 |
|
| 28 |
public virtual DbSet<Project> Projects { get; set; }
|
| 29 |
|
|
|
|
| 31 |
|
| 32 |
public virtual DbSet<Role> Roles { get; set; }
|
| 33 |
|
| 34 |
+
public virtual DbSet<RoleMenu> RoleMenus { get; set; }
|
| 35 |
|
| 36 |
public virtual DbSet<Task> Tasks { get; set; }
|
| 37 |
|
| 38 |
public virtual DbSet<TaskHistory> TaskHistories { get; set; }
|
| 39 |
|
| 40 |
+
public virtual DbSet<TimeLog> TimeLogs { get; set; }
|
| 41 |
|
| 42 |
+
public virtual DbSet<User> Users { get; set; }
|
|
|
|
|
|
|
|
|
|
| 43 |
|
|
|
|
|
|
|
|
|
|
| 44 |
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
| 45 |
+
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
|
| 46 |
+
=> optionsBuilder.UseSqlServer("Server=.;Database=TTS;User Id=sa;Password=sasa@123;TrustServerCertificate=True;");
|
| 47 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
| 49 |
{
|
| 50 |
modelBuilder.Entity<AuditLog>(entity =>
|
| 51 |
{
|
| 52 |
+
entity.HasIndex(e => e.UserId, "IX_AuditLogs_UserId");
|
| 53 |
|
| 54 |
entity.Property(e => e.Action).HasMaxLength(100);
|
| 55 |
entity.Property(e => e.CreatedAt)
|
|
|
|
| 62 |
|
| 63 |
entity.HasOne(d => d.User).WithMany(p => p.AuditLogs)
|
| 64 |
.HasForeignKey(d => d.UserId)
|
| 65 |
+
.HasConstraintName("FK_AuditLogs_Users");
|
| 66 |
});
|
| 67 |
|
| 68 |
modelBuilder.Entity<Comment>(entity =>
|
| 69 |
{
|
| 70 |
+
entity.HasIndex(e => e.TaskId, "IX_Comments_TaskId");
|
| 71 |
+
|
| 72 |
+
entity.HasIndex(e => e.UserId, "IX_Comments_UserId");
|
| 73 |
|
| 74 |
entity.Property(e => e.CreatedAt)
|
| 75 |
.HasDefaultValueSql("(getdate())")
|
| 76 |
.HasColumnType("datetime");
|
|
|
|
| 77 |
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
| 78 |
|
| 79 |
entity.HasOne(d => d.Task).WithMany(p => p.Comments)
|
| 80 |
.HasForeignKey(d => d.TaskId)
|
| 81 |
+
.HasConstraintName("FK_Comments_Tasks");
|
| 82 |
|
| 83 |
entity.HasOne(d => d.User).WithMany(p => p.Comments)
|
| 84 |
.HasForeignKey(d => d.UserId)
|
| 85 |
.OnDelete(DeleteBehavior.ClientSetNull)
|
| 86 |
+
.HasConstraintName("FK_Comments_Users");
|
| 87 |
});
|
| 88 |
|
| 89 |
modelBuilder.Entity<FileAttachment>(entity =>
|
| 90 |
{
|
| 91 |
+
entity.HasIndex(e => e.TaskId, "IX_FileAttachments_TaskId");
|
| 92 |
|
| 93 |
entity.Property(e => e.CreatedAt)
|
| 94 |
.HasDefaultValueSql("(getdate())")
|
| 95 |
.HasColumnType("datetime");
|
| 96 |
entity.Property(e => e.FileName).HasMaxLength(255);
|
| 97 |
entity.Property(e => e.FileType).HasMaxLength(100);
|
|
|
|
| 98 |
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
| 99 |
|
| 100 |
entity.HasOne(d => d.Task).WithMany(p => p.FileAttachments)
|
| 101 |
.HasForeignKey(d => d.TaskId)
|
| 102 |
.OnDelete(DeleteBehavior.ClientSetNull)
|
| 103 |
+
.HasConstraintName("FK_FileAttachments_Tasks");
|
| 104 |
});
|
| 105 |
|
| 106 |
+
modelBuilder.Entity<MenuAdmin>(entity =>
|
| 107 |
{
|
| 108 |
+
entity.HasKey(e => e.AdminMenuId);
|
| 109 |
|
| 110 |
+
entity.Property(e => e.AdminMenuId).HasMaxLength(50);
|
| 111 |
+
entity.Property(e => e.CreatedUserId).HasMaxLength(50);
|
| 112 |
+
entity.Property(e => e.Icon).HasMaxLength(50);
|
| 113 |
+
entity.Property(e => e.MenuCode).HasMaxLength(50);
|
| 114 |
+
entity.Property(e => e.MenuName).HasMaxLength(100);
|
| 115 |
+
entity.Property(e => e.MenuUrl).HasMaxLength(200);
|
| 116 |
+
entity.Property(e => e.ModifiedUserId).HasMaxLength(50);
|
| 117 |
+
entity.Property(e => e.ParentCode).HasMaxLength(50);
|
| 118 |
+
});
|
| 119 |
|
| 120 |
+
modelBuilder.Entity<MenuAdminDetail>(entity =>
|
| 121 |
+
{
|
| 122 |
+
entity.Property(e => e.MenuAdminDetailId).HasMaxLength(50);
|
| 123 |
+
entity.Property(e => e.ActionName).HasMaxLength(50);
|
| 124 |
+
entity.Property(e => e.ApiName).HasMaxLength(100);
|
| 125 |
+
entity.Property(e => e.CreatedUserId).HasMaxLength(50);
|
| 126 |
+
entity.Property(e => e.MenuDetailCode).HasMaxLength(50);
|
| 127 |
+
entity.Property(e => e.ModifiedUserId).HasMaxLength(50);
|
| 128 |
+
entity.Property(e => e.ParentMenuCode).HasMaxLength(50);
|
| 129 |
});
|
| 130 |
|
| 131 |
modelBuilder.Entity<Project>(entity =>
|
| 132 |
{
|
| 133 |
+
entity.HasIndex(e => e.CreatedById, "IX_Projects_CreatedById");
|
| 134 |
|
| 135 |
entity.Property(e => e.CreatedAt)
|
| 136 |
.HasDefaultValueSql("(getdate())")
|
|
|
|
| 139 |
.HasDefaultValueSql("(getdate())")
|
| 140 |
.HasColumnType("datetime");
|
| 141 |
entity.Property(e => e.EndDate).HasColumnType("datetime");
|
|
|
|
| 142 |
entity.Property(e => e.Name).HasMaxLength(150);
|
| 143 |
entity.Property(e => e.StartDate).HasColumnType("datetime");
|
| 144 |
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
| 145 |
|
| 146 |
entity.HasOne(d => d.CreatedByNavigation).WithMany(p => p.Projects)
|
| 147 |
.HasForeignKey(d => d.CreatedById)
|
| 148 |
+
.OnDelete(DeleteBehavior.ClientSetNull);
|
|
|
|
| 149 |
});
|
| 150 |
|
| 151 |
modelBuilder.Entity<ProjectMember>(entity =>
|
| 152 |
{
|
| 153 |
+
entity.HasKey(e => new { e.ProjectId, e.UserId });
|
| 154 |
+
|
| 155 |
+
entity.HasIndex(e => e.UserId, "IX_ProjectMembers_UserId");
|
| 156 |
|
| 157 |
entity.Property(e => e.CreatedAt)
|
| 158 |
.HasDefaultValueSql("(getdate())")
|
|
|
|
| 160 |
|
| 161 |
entity.HasOne(d => d.Project).WithMany(p => p.ProjectMembers)
|
| 162 |
.HasForeignKey(d => d.ProjectId)
|
| 163 |
+
.HasConstraintName("FK_ProjectMembers_Projects");
|
| 164 |
|
| 165 |
entity.HasOne(d => d.User).WithMany(p => p.ProjectMembers)
|
| 166 |
.HasForeignKey(d => d.UserId)
|
| 167 |
+
.HasConstraintName("FK_ProjectMembers_Users");
|
| 168 |
});
|
| 169 |
|
| 170 |
modelBuilder.Entity<Role>(entity =>
|
| 171 |
{
|
| 172 |
+
entity.HasIndex(e => e.Name, "UQ_Roles_Name").IsUnique();
|
|
|
|
|
|
|
| 173 |
|
| 174 |
entity.Property(e => e.CreatedAt)
|
| 175 |
.HasDefaultValueSql("(getdate())")
|
| 176 |
.HasColumnType("datetime");
|
| 177 |
entity.Property(e => e.Description).HasMaxLength(200);
|
|
|
|
| 178 |
entity.Property(e => e.Name).HasMaxLength(50);
|
| 179 |
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
| 180 |
});
|
| 181 |
|
| 182 |
+
modelBuilder.Entity<RoleMenu>(entity =>
|
| 183 |
{
|
| 184 |
+
entity.Property(e => e.RoleMenuId).HasMaxLength(50);
|
| 185 |
+
entity.Property(e => e.CreatedUserId).HasMaxLength(50);
|
| 186 |
+
entity.Property(e => e.MenuCode).HasMaxLength(50);
|
| 187 |
+
entity.Property(e => e.ModifiedUserId).HasMaxLength(50);
|
| 188 |
+
entity.Property(e => e.RoleCode).HasMaxLength(50);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
});
|
| 190 |
|
| 191 |
modelBuilder.Entity<Task>(entity =>
|
| 192 |
{
|
| 193 |
+
entity.HasIndex(e => e.AssignedBy, "IX_Tasks_AssignedBy");
|
| 194 |
+
|
| 195 |
+
entity.HasIndex(e => e.AssignedTo, "IX_Tasks_AssignedTo");
|
| 196 |
+
|
| 197 |
+
entity.HasIndex(e => e.ProjectId, "IX_Tasks_ProjectId");
|
| 198 |
|
| 199 |
entity.Property(e => e.CreatedAt)
|
| 200 |
.HasDefaultValueSql("(getdate())")
|
| 201 |
.HasColumnType("datetime");
|
| 202 |
entity.Property(e => e.DueDate).HasColumnType("datetime");
|
| 203 |
+
entity.Property(e => e.EstimatedHours).HasColumnType("decimal(5, 2)");
|
| 204 |
entity.Property(e => e.PriorityId).HasDefaultValue(2L);
|
| 205 |
entity.Property(e => e.StatusId).HasDefaultValue(1L);
|
| 206 |
entity.Property(e => e.Title).HasMaxLength(200);
|
| 207 |
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
| 208 |
|
| 209 |
+
entity.HasOne(d => d.AssignedByNavigation).WithMany(p => p.TaskAssignedByNavigations).HasForeignKey(d => d.AssignedBy);
|
|
|
|
|
|
|
| 210 |
|
| 211 |
+
entity.HasOne(d => d.AssignedToNavigation).WithMany(p => p.TaskAssignedToNavigations).HasForeignKey(d => d.AssignedTo);
|
|
|
|
|
|
|
| 212 |
|
| 213 |
entity.HasOne(d => d.Project).WithMany(p => p.Tasks)
|
| 214 |
.HasForeignKey(d => d.ProjectId)
|
| 215 |
+
.HasConstraintName("FK_Tasks_Projects");
|
| 216 |
});
|
| 217 |
|
| 218 |
modelBuilder.Entity<TaskHistory>(entity =>
|
| 219 |
{
|
| 220 |
+
entity.HasIndex(e => e.TaskId, "IX_TaskHistories_TaskId");
|
| 221 |
|
| 222 |
entity.Property(e => e.CreatedAt)
|
| 223 |
.HasDefaultValueSql("(getdate())")
|
|
|
|
| 226 |
|
| 227 |
entity.HasOne(d => d.CreatedByNavigation).WithMany(p => p.TaskHistoryCreatedByNavigations)
|
| 228 |
.HasForeignKey(d => d.CreatedBy)
|
| 229 |
+
.OnDelete(DeleteBehavior.ClientSetNull);
|
|
|
|
| 230 |
|
| 231 |
entity.HasOne(d => d.ModifiedBy).WithMany(p => p.TaskHistoryModifiedBies)
|
| 232 |
.HasForeignKey(d => d.ModifiedById)
|
| 233 |
.OnDelete(DeleteBehavior.ClientSetNull)
|
| 234 |
+
.HasConstraintName("FK_TaskHistories_Users_ModifiedBy");
|
| 235 |
|
| 236 |
entity.HasOne(d => d.Task).WithMany(p => p.TaskHistories)
|
| 237 |
.HasForeignKey(d => d.TaskId)
|
| 238 |
+
.HasConstraintName("FK_TaskHistories_Tasks");
|
| 239 |
+
});
|
| 240 |
+
|
| 241 |
+
modelBuilder.Entity<TimeLog>(entity =>
|
| 242 |
+
{
|
| 243 |
+
entity.HasIndex(e => e.LogDate, "IX_TimeLogs_LogDate");
|
| 244 |
+
|
| 245 |
+
entity.HasIndex(e => e.TaskId, "IX_TimeLogs_TaskId");
|
| 246 |
+
|
| 247 |
+
entity.HasIndex(e => e.UserId, "IX_TimeLogs_UserId");
|
| 248 |
+
|
| 249 |
+
entity.Property(e => e.CreatedAt)
|
| 250 |
+
.HasDefaultValueSql("(getdate())")
|
| 251 |
+
.HasColumnType("datetime");
|
| 252 |
+
entity.Property(e => e.HoursLogged).HasColumnType("decimal(5, 2)");
|
| 253 |
+
entity.Property(e => e.Notes).HasMaxLength(500);
|
| 254 |
+
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
| 255 |
+
|
| 256 |
+
entity.HasOne(d => d.Task).WithMany(p => p.TimeLogs)
|
| 257 |
+
.HasForeignKey(d => d.TaskId)
|
| 258 |
+
.HasConstraintName("FK_TimeLogs_Tasks");
|
| 259 |
+
|
| 260 |
+
entity.HasOne(d => d.User).WithMany(p => p.TimeLogs)
|
| 261 |
+
.HasForeignKey(d => d.UserId)
|
| 262 |
+
.OnDelete(DeleteBehavior.ClientSetNull)
|
| 263 |
+
.HasConstraintName("FK_TimeLogs_Users");
|
| 264 |
});
|
| 265 |
|
| 266 |
modelBuilder.Entity<User>(entity =>
|
| 267 |
{
|
| 268 |
+
entity.HasIndex(e => e.RoleId, "IX_Users_RoleId");
|
| 269 |
|
| 270 |
+
entity.HasIndex(e => e.Email, "UQ_Users_Email").IsUnique();
|
| 271 |
|
| 272 |
+
entity.HasIndex(e => e.Username, "UQ_Users_Username").IsUnique();
|
| 273 |
|
| 274 |
entity.Property(e => e.CreatedAt)
|
| 275 |
.HasDefaultValueSql("(getdate())")
|
|
|
|
| 277 |
entity.Property(e => e.Email).HasMaxLength(256);
|
| 278 |
entity.Property(e => e.FirstName).HasMaxLength(50);
|
| 279 |
entity.Property(e => e.IsActive).HasDefaultValue(true);
|
|
|
|
| 280 |
entity.Property(e => e.LastName).HasMaxLength(50);
|
| 281 |
entity.Property(e => e.Phone).HasMaxLength(20);
|
| 282 |
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
|
|
|
| 285 |
entity.HasOne(d => d.Role).WithMany(p => p.Users)
|
| 286 |
.HasForeignKey(d => d.RoleId)
|
| 287 |
.OnDelete(DeleteBehavior.ClientSetNull)
|
| 288 |
+
.HasConstraintName("FK_Users_Roles");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
});
|
| 290 |
|
| 291 |
OnModelCreatingPartial(modelBuilder);
|
TaskTrackingSystem.Database/AppDbContextModels/Comment.cs
CHANGED
|
@@ -21,7 +21,7 @@ public partial class Comment
|
|
| 21 |
|
| 22 |
public long? UpdatedBy { get; set; }
|
| 23 |
|
| 24 |
-
public bool
|
| 25 |
|
| 26 |
public virtual Task Task { get; set; } = null!;
|
| 27 |
|
|
|
|
| 21 |
|
| 22 |
public long? UpdatedBy { get; set; }
|
| 23 |
|
| 24 |
+
public bool IsDeleted { get; set; }
|
| 25 |
|
| 26 |
public virtual Task Task { get; set; } = null!;
|
| 27 |
|
TaskTrackingSystem.Database/AppDbContextModels/FileAttachment.cs
CHANGED
|
@@ -25,7 +25,7 @@ public partial class FileAttachment
|
|
| 25 |
|
| 26 |
public long? UpdatedBy { get; set; }
|
| 27 |
|
| 28 |
-
public bool
|
| 29 |
|
| 30 |
public virtual Task Task { get; set; } = null!;
|
| 31 |
}
|
|
|
|
| 25 |
|
| 26 |
public long? UpdatedBy { get; set; }
|
| 27 |
|
| 28 |
+
public bool IsDeleted { get; set; }
|
| 29 |
|
| 30 |
public virtual Task Task { get; set; } = null!;
|
| 31 |
}
|
TaskTrackingSystem.Database/AppDbContextModels/MenuAdmin.cs
CHANGED
|
@@ -1,35 +1,33 @@
|
|
| 1 |
using System;
|
| 2 |
using System.Collections.Generic;
|
| 3 |
-
using System.Linq;
|
| 4 |
-
using System.Text;
|
| 5 |
-
using System.Threading.Tasks;
|
| 6 |
|
| 7 |
-
namespace TaskTrackingSystem.Database.AppDbContextModels
|
|
|
|
|
|
|
| 8 |
{
|
| 9 |
-
public
|
| 10 |
-
{
|
| 11 |
-
public string AdminMenuId { get; set; } = null!;
|
| 12 |
|
| 13 |
-
|
| 14 |
|
| 15 |
-
|
| 16 |
|
| 17 |
-
|
| 18 |
|
| 19 |
-
|
| 20 |
|
| 21 |
-
|
| 22 |
|
| 23 |
-
|
| 24 |
|
| 25 |
-
|
| 26 |
|
| 27 |
-
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
public DateTime? ModifiedDateTime { get; set; }
|
| 33 |
-
}
|
| 34 |
-
}
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
using System;
|
| 2 |
using System.Collections.Generic;
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
+
namespace TaskTrackingSystem.Database.AppDbContextModels;
|
| 5 |
+
|
| 6 |
+
public partial class MenuAdmin
|
| 7 |
{
|
| 8 |
+
public string AdminMenuId { get; set; } = null!;
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
public string MenuCode { get; set; } = null!;
|
| 11 |
|
| 12 |
+
public string ParentCode { get; set; } = null!;
|
| 13 |
|
| 14 |
+
public string MenuName { get; set; } = null!;
|
| 15 |
|
| 16 |
+
public string? MenuUrl { get; set; }
|
| 17 |
|
| 18 |
+
public bool Visible { get; set; }
|
| 19 |
|
| 20 |
+
public int OrderNo { get; set; }
|
| 21 |
|
| 22 |
+
public string? Icon { get; set; }
|
| 23 |
|
| 24 |
+
public int DelFlag { get; set; }
|
| 25 |
|
| 26 |
+
public string? CreatedUserId { get; set; }
|
| 27 |
+
|
| 28 |
+
public DateTime CreatedDateTime { get; set; }
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
+
public string? ModifiedUserId { get; set; }
|
| 31 |
+
|
| 32 |
+
public DateTime? ModifiedDateTime { get; set; }
|
| 33 |
+
}
|
TaskTrackingSystem.Database/AppDbContextModels/MenuAdminDetail.cs
CHANGED
|
@@ -1,34 +1,31 @@
|
|
| 1 |
using System;
|
| 2 |
using System.Collections.Generic;
|
| 3 |
-
using System.Linq;
|
| 4 |
-
using System.Text;
|
| 5 |
-
using System.Threading.Tasks;
|
| 6 |
|
| 7 |
-
namespace TaskTrackingSystem.Database.AppDbContextModels
|
|
|
|
|
|
|
| 8 |
{
|
| 9 |
-
public
|
| 10 |
-
{
|
| 11 |
-
public string MenuAdminDetailId { get; set; } = null!;
|
| 12 |
|
| 13 |
-
|
| 14 |
|
| 15 |
-
|
| 16 |
|
| 17 |
-
|
| 18 |
|
| 19 |
-
|
| 20 |
|
| 21 |
-
|
| 22 |
|
| 23 |
-
|
| 24 |
|
| 25 |
-
|
| 26 |
|
| 27 |
-
|
| 28 |
-
public DateTime CreatedDateTime { get; set; }
|
| 29 |
-
public string? ModifiedUserId { get; set; }
|
| 30 |
-
public DateTime? ModifiedDateTime { get; set; }
|
| 31 |
-
}
|
| 32 |
-
}
|
| 33 |
|
|
|
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
using System;
|
| 2 |
using System.Collections.Generic;
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
+
namespace TaskTrackingSystem.Database.AppDbContextModels;
|
| 5 |
+
|
| 6 |
+
public partial class MenuAdminDetail
|
| 7 |
{
|
| 8 |
+
public string MenuAdminDetailId { get; set; } = null!;
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
public string MenuDetailCode { get; set; } = null!;
|
| 11 |
|
| 12 |
+
public string ParentMenuCode { get; set; } = null!;
|
| 13 |
|
| 14 |
+
public string ActionName { get; set; } = null!;
|
| 15 |
|
| 16 |
+
public string ApiName { get; set; } = null!;
|
| 17 |
|
| 18 |
+
public bool Visible { get; set; }
|
| 19 |
|
| 20 |
+
public int OrderNo { get; set; }
|
| 21 |
|
| 22 |
+
public int DelFlag { get; set; }
|
| 23 |
|
| 24 |
+
public string? CreatedUserId { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
public DateTime CreatedDateTime { get; set; }
|
| 27 |
|
| 28 |
+
public string? ModifiedUserId { get; set; }
|
| 29 |
+
|
| 30 |
+
public DateTime? ModifiedDateTime { get; set; }
|
| 31 |
+
}
|
TaskTrackingSystem.Database/AppDbContextModels/Project.cs
CHANGED
|
@@ -27,7 +27,9 @@ public partial class Project
|
|
| 27 |
|
| 28 |
public long? UpdatedBy { get; set; }
|
| 29 |
|
| 30 |
-
public bool
|
|
|
|
|
|
|
| 31 |
|
| 32 |
public virtual User CreatedByNavigation { get; set; } = null!;
|
| 33 |
|
|
|
|
| 27 |
|
| 28 |
public long? UpdatedBy { get; set; }
|
| 29 |
|
| 30 |
+
public bool IsDeleted { get; set; }
|
| 31 |
+
|
| 32 |
+
public int? BudgetedHours { get; set; }
|
| 33 |
|
| 34 |
public virtual User CreatedByNavigation { get; set; } = null!;
|
| 35 |
|
TaskTrackingSystem.Database/AppDbContextModels/Role.cs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 2 |
using System.Collections.Generic;
|
| 3 |
|
| 4 |
namespace TaskTrackingSystem.Database.AppDbContextModels;
|
|
@@ -21,7 +21,5 @@ public partial class Role
|
|
| 21 |
|
| 22 |
public bool IsDeleted { get; set; }
|
| 23 |
|
| 24 |
-
public virtual ICollection<RolePermission> RolePermissions { get; set; } = new List<RolePermission>();
|
| 25 |
-
|
| 26 |
public virtual ICollection<User> Users { get; set; } = new List<User>();
|
| 27 |
}
|
|
|
|
| 1 |
+
using System;
|
| 2 |
using System.Collections.Generic;
|
| 3 |
|
| 4 |
namespace TaskTrackingSystem.Database.AppDbContextModels;
|
|
|
|
| 21 |
|
| 22 |
public bool IsDeleted { get; set; }
|
| 23 |
|
|
|
|
|
|
|
| 24 |
public virtual ICollection<User> Users { get; set; } = new List<User>();
|
| 25 |
}
|
TaskTrackingSystem.Database/AppDbContextModels/RoleMenu.cs
CHANGED
|
@@ -1,21 +1,23 @@
|
|
| 1 |
using System;
|
| 2 |
using System.Collections.Generic;
|
| 3 |
-
using System.Linq;
|
| 4 |
-
using System.Text;
|
| 5 |
-
using System.Threading.Tasks;
|
| 6 |
|
| 7 |
-
namespace TaskTrackingSystem.Database.AppDbContextModels
|
|
|
|
|
|
|
| 8 |
{
|
| 9 |
-
public
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
| 21 |
}
|
|
|
|
| 1 |
using System;
|
| 2 |
using System.Collections.Generic;
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
+
namespace TaskTrackingSystem.Database.AppDbContextModels;
|
| 5 |
+
|
| 6 |
+
public partial class RoleMenu
|
| 7 |
{
|
| 8 |
+
public string RoleMenuId { get; set; } = null!;
|
| 9 |
+
|
| 10 |
+
public string RoleCode { get; set; } = null!;
|
| 11 |
+
|
| 12 |
+
public string MenuCode { get; set; } = null!;
|
| 13 |
+
|
| 14 |
+
public int DelFlag { get; set; }
|
| 15 |
+
|
| 16 |
+
public string? CreatedUserId { get; set; }
|
| 17 |
+
|
| 18 |
+
public DateTime CreatedDateTime { get; set; }
|
| 19 |
+
|
| 20 |
+
public string? ModifiedUserId { get; set; }
|
| 21 |
+
|
| 22 |
+
public DateTime? ModifiedDateTime { get; set; }
|
| 23 |
}
|
TaskTrackingSystem.Database/AppDbContextModels/Task.cs
CHANGED
|
@@ -21,7 +21,7 @@ public partial class Task
|
|
| 21 |
|
| 22 |
public long? AssignedBy { get; set; }
|
| 23 |
|
| 24 |
-
public
|
| 25 |
|
| 26 |
public DateTime DueDate { get; set; }
|
| 27 |
|
|
@@ -46,4 +46,6 @@ public partial class Task
|
|
| 46 |
public virtual Project Project { get; set; } = null!;
|
| 47 |
|
| 48 |
public virtual ICollection<TaskHistory> TaskHistories { get; set; } = new List<TaskHistory>();
|
|
|
|
|
|
|
| 49 |
}
|
|
|
|
| 21 |
|
| 22 |
public long? AssignedBy { get; set; }
|
| 23 |
|
| 24 |
+
public decimal? EstimatedHours { get; set; }
|
| 25 |
|
| 26 |
public DateTime DueDate { get; set; }
|
| 27 |
|
|
|
|
| 46 |
public virtual Project Project { get; set; } = null!;
|
| 47 |
|
| 48 |
public virtual ICollection<TaskHistory> TaskHistories { get; set; } = new List<TaskHistory>();
|
| 49 |
+
|
| 50 |
+
public virtual ICollection<TimeLog> TimeLogs { get; set; } = new List<TimeLog>();
|
| 51 |
}
|
TaskTrackingSystem.Database/AppDbContextModels/TimeLog.cs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using System;
|
| 2 |
+
using System.Collections.Generic;
|
| 3 |
+
|
| 4 |
+
namespace TaskTrackingSystem.Database.AppDbContextModels;
|
| 5 |
+
|
| 6 |
+
public partial class TimeLog
|
| 7 |
+
{
|
| 8 |
+
public long Id { get; set; }
|
| 9 |
+
|
| 10 |
+
public long TaskId { get; set; }
|
| 11 |
+
|
| 12 |
+
public long UserId { get; set; }
|
| 13 |
+
|
| 14 |
+
public decimal HoursLogged { get; set; }
|
| 15 |
+
|
| 16 |
+
public DateOnly LogDate { get; set; }
|
| 17 |
+
|
| 18 |
+
public string? Notes { get; set; }
|
| 19 |
+
|
| 20 |
+
public DateTime? CreatedAt { get; set; }
|
| 21 |
+
|
| 22 |
+
public long? CreatedBy { get; set; }
|
| 23 |
+
|
| 24 |
+
public DateTime? UpdatedAt { get; set; }
|
| 25 |
+
|
| 26 |
+
public long? UpdatedBy { get; set; }
|
| 27 |
+
|
| 28 |
+
public bool IsDeleted { get; set; }
|
| 29 |
+
|
| 30 |
+
public virtual Task Task { get; set; } = null!;
|
| 31 |
+
|
| 32 |
+
public virtual User User { get; set; } = null!;
|
| 33 |
+
}
|
TaskTrackingSystem.Database/AppDbContextModels/User.cs
CHANGED
|
@@ -7,6 +7,8 @@ public partial class User
|
|
| 7 |
{
|
| 8 |
public long Id { get; set; }
|
| 9 |
|
|
|
|
|
|
|
| 10 |
public string FirstName { get; set; } = null!;
|
| 11 |
|
| 12 |
public string LastName { get; set; } = null!;
|
|
@@ -31,8 +33,6 @@ public partial class User
|
|
| 31 |
|
| 32 |
public bool IsDeleted { get; set; }
|
| 33 |
|
| 34 |
-
public string Username { get; set; } = null!;
|
| 35 |
-
|
| 36 |
public virtual ICollection<AuditLog> AuditLogs { get; set; } = new List<AuditLog>();
|
| 37 |
|
| 38 |
public virtual ICollection<Comment> Comments { get; set; } = new List<Comment>();
|
|
@@ -50,4 +50,6 @@ public partial class User
|
|
| 50 |
public virtual ICollection<TaskHistory> TaskHistoryCreatedByNavigations { get; set; } = new List<TaskHistory>();
|
| 51 |
|
| 52 |
public virtual ICollection<TaskHistory> TaskHistoryModifiedBies { get; set; } = new List<TaskHistory>();
|
|
|
|
|
|
|
| 53 |
}
|
|
|
|
| 7 |
{
|
| 8 |
public long Id { get; set; }
|
| 9 |
|
| 10 |
+
public string Username { get; set; } = null!;
|
| 11 |
+
|
| 12 |
public string FirstName { get; set; } = null!;
|
| 13 |
|
| 14 |
public string LastName { get; set; } = null!;
|
|
|
|
| 33 |
|
| 34 |
public bool IsDeleted { get; set; }
|
| 35 |
|
|
|
|
|
|
|
| 36 |
public virtual ICollection<AuditLog> AuditLogs { get; set; } = new List<AuditLog>();
|
| 37 |
|
| 38 |
public virtual ICollection<Comment> Comments { get; set; } = new List<Comment>();
|
|
|
|
| 50 |
public virtual ICollection<TaskHistory> TaskHistoryCreatedByNavigations { get; set; } = new List<TaskHistory>();
|
| 51 |
|
| 52 |
public virtual ICollection<TaskHistory> TaskHistoryModifiedBies { get; set; } = new List<TaskHistory>();
|
| 53 |
+
|
| 54 |
+
public virtual ICollection<TimeLog> TimeLogs { get; set; } = new List<TimeLog>();
|
| 55 |
}
|
TaskTrackingSystem.Shared/Models/Project/CreateProjectDto.cs
CHANGED
|
@@ -22,5 +22,7 @@ namespace TaskTrackingSystem.Shared.Models.Project
|
|
| 22 |
|
| 23 |
[Required]
|
| 24 |
public long CreatedById { get; set; }
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
}
|
|
|
|
| 22 |
|
| 23 |
[Required]
|
| 24 |
public long CreatedById { get; set; }
|
| 25 |
+
|
| 26 |
+
public int? BudgetedHours { get; set; }
|
| 27 |
}
|
| 28 |
}
|
TaskTrackingSystem.Shared/Models/Project/ProjectDto.cs
CHANGED
|
@@ -15,5 +15,6 @@ namespace TaskTrackingSystem.Shared.Models.Project
|
|
| 15 |
public DateTime EndDate { get; set; }
|
| 16 |
public long CreatedById { get; set; }
|
| 17 |
public DateTime CreatedAt { get; set; }
|
|
|
|
| 18 |
}
|
| 19 |
}
|
|
|
|
| 15 |
public DateTime EndDate { get; set; }
|
| 16 |
public long CreatedById { get; set; }
|
| 17 |
public DateTime CreatedAt { get; set; }
|
| 18 |
+
public int? BudgetedHours { get; set; }
|
| 19 |
}
|
| 20 |
}
|
TaskTrackingSystem.Shared/Models/Project/UpdateProjectDto.cs
CHANGED
|
@@ -19,5 +19,7 @@ namespace TaskTrackingSystem.Shared.Models.Project
|
|
| 19 |
|
| 20 |
[Required]
|
| 21 |
public DateTime EndDate { get; set; }
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
}
|
|
|
|
| 19 |
|
| 20 |
[Required]
|
| 21 |
public DateTime EndDate { get; set; }
|
| 22 |
+
|
| 23 |
+
public int? BudgetedHours { get; set; }
|
| 24 |
}
|
| 25 |
}
|
TaskTrackingSystem.Shared/Models/Report/TaskReportDto.cs
CHANGED
|
@@ -16,7 +16,7 @@ namespace TaskTrackingSystem.Shared.Models.Report
|
|
| 16 |
public string? AssignedToUser { get; set; }
|
| 17 |
public long? AssignedToUserId { get; set; }
|
| 18 |
public string? AssignedByUser { get; set; }
|
| 19 |
-
public
|
| 20 |
public DateTime DueDate { get; set; }
|
| 21 |
public DateTime CreatedAt { get; set; }
|
| 22 |
}
|
|
|
|
| 16 |
public string? AssignedToUser { get; set; }
|
| 17 |
public long? AssignedToUserId { get; set; }
|
| 18 |
public string? AssignedByUser { get; set; }
|
| 19 |
+
public decimal? EstimatedHours { get; set; }
|
| 20 |
public DateTime DueDate { get; set; }
|
| 21 |
public DateTime CreatedAt { get; set; }
|
| 22 |
}
|
TaskTrackingSystem.Shared/Models/Report/TimeTrackingReportDto.cs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using System;
|
| 2 |
+
using System.Collections.Generic;
|
| 3 |
+
|
| 4 |
+
namespace TaskTrackingSystem.Shared.Models.Report
|
| 5 |
+
{
|
| 6 |
+
public class TimeTrackingReportDto
|
| 7 |
+
{
|
| 8 |
+
public decimal TotalEstimatedHrs { get; set; }
|
| 9 |
+
public decimal CompletedHours { get; set; }
|
| 10 |
+
public decimal Variance { get; set; }
|
| 11 |
+
public double CompletionPercentage { get; set; }
|
| 12 |
+
public int TasksWithHours { get; set; }
|
| 13 |
+
public decimal AvgHoursPerTask { get; set; }
|
| 14 |
+
|
| 15 |
+
public List<EmployeeTimeSummaryDto> EmployeeSummary { get; set; } = new();
|
| 16 |
+
public List<ProjectTimeSummaryDto> ProjectSummary { get; set; } = new();
|
| 17 |
+
public List<TaskTimeDetailDto> TaskDetail { get; set; } = new();
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
public class EmployeeTimeSummaryDto
|
| 21 |
+
{
|
| 22 |
+
public long UserId { get; set; }
|
| 23 |
+
public string FullName { get; set; } = string.Empty;
|
| 24 |
+
public string Username { get; set; } = string.Empty;
|
| 25 |
+
public int TotalTasks { get; set; }
|
| 26 |
+
public int CompletedTasks { get; set; }
|
| 27 |
+
public decimal EstHours { get; set; }
|
| 28 |
+
public decimal CompletedHours { get; set; }
|
| 29 |
+
public decimal Variance { get; set; }
|
| 30 |
+
public double CompletionPercentage { get; set; }
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
public class ProjectTimeSummaryDto
|
| 34 |
+
{
|
| 35 |
+
public long ProjectId { get; set; }
|
| 36 |
+
public string ProjectName { get; set; } = string.Empty;
|
| 37 |
+
public int TotalTasks { get; set; }
|
| 38 |
+
public int CompletedTasks { get; set; }
|
| 39 |
+
public decimal EstHours { get; set; }
|
| 40 |
+
public decimal CompletedHours { get; set; }
|
| 41 |
+
public decimal Variance { get; set; }
|
| 42 |
+
public double CompletionPercentage { get; set; }
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
public class TaskTimeDetailDto
|
| 46 |
+
{
|
| 47 |
+
public long TaskId { get; set; }
|
| 48 |
+
public string Title { get; set; } = string.Empty;
|
| 49 |
+
public long ProjectId { get; set; }
|
| 50 |
+
public string ProjectName { get; set; } = string.Empty;
|
| 51 |
+
public long? AssignedTo { get; set; }
|
| 52 |
+
public string AssigneeName { get; set; } = string.Empty;
|
| 53 |
+
public string AssigneeUsername { get; set; } = string.Empty;
|
| 54 |
+
public decimal? EstimatedHours { get; set; }
|
| 55 |
+
public decimal CompletedHours { get; set; }
|
| 56 |
+
public DateTime DueDate { get; set; }
|
| 57 |
+
public long StatusId { get; set; }
|
| 58 |
+
}
|
| 59 |
+
}
|
TaskTrackingSystem.Shared/Models/Report/UserProductivityDto.cs
CHANGED
|
@@ -8,5 +8,6 @@ namespace TaskTrackingSystem.Shared.Models.Report
|
|
| 8 |
public int TotalAssignedTasks { get; set; }
|
| 9 |
public int CompletedTasksCount { get; set; }
|
| 10 |
public double EfficiencyRatio { get; set; } // Completed / Total Asssined (percentage or ratio)
|
|
|
|
| 11 |
}
|
| 12 |
}
|
|
|
|
| 8 |
public int TotalAssignedTasks { get; set; }
|
| 9 |
public int CompletedTasksCount { get; set; }
|
| 10 |
public double EfficiencyRatio { get; set; } // Completed / Total Asssined (percentage or ratio)
|
| 11 |
+
public double OnTimeDeliveryRate { get; set; }
|
| 12 |
}
|
| 13 |
}
|
TaskTrackingSystem.Shared/Models/Task/CreateTaskDto.cs
CHANGED
|
@@ -25,7 +25,7 @@ namespace TaskTrackingSystem.Shared.Models.Task
|
|
| 25 |
|
| 26 |
public long? AssignedBy { get; set; }
|
| 27 |
|
| 28 |
-
public
|
| 29 |
|
| 30 |
[Required]
|
| 31 |
public DateTime DueDate { get; set; }
|
|
|
|
| 25 |
|
| 26 |
public long? AssignedBy { get; set; }
|
| 27 |
|
| 28 |
+
public decimal? EstimatedHours { get; set; }
|
| 29 |
|
| 30 |
[Required]
|
| 31 |
public DateTime DueDate { get; set; }
|
TaskTrackingSystem.Shared/Models/Task/TaskDto.cs
CHANGED
|
@@ -16,8 +16,9 @@ namespace TaskTrackingSystem.Shared.Models.Task
|
|
| 16 |
public long PriorityId { get; set; }
|
| 17 |
public long? AssignedTo { get; set; }
|
| 18 |
public long? AssignedBy { get; set; }
|
| 19 |
-
public
|
| 20 |
public DateTime DueDate { get; set; }
|
| 21 |
public DateTime CreatedAt { get; set; }
|
|
|
|
| 22 |
}
|
| 23 |
}
|
|
|
|
| 16 |
public long PriorityId { get; set; }
|
| 17 |
public long? AssignedTo { get; set; }
|
| 18 |
public long? AssignedBy { get; set; }
|
| 19 |
+
public decimal? EstimatedHours { get; set; }
|
| 20 |
public DateTime DueDate { get; set; }
|
| 21 |
public DateTime CreatedAt { get; set; }
|
| 22 |
+
public DateTime? CompletedAt { get; set; }
|
| 23 |
}
|
| 24 |
}
|
TaskTrackingSystem.Shared/Models/Task/UpdateTaskDto.cs
CHANGED
|
@@ -24,7 +24,7 @@ namespace TaskTrackingSystem.Shared.Models.Task
|
|
| 24 |
|
| 25 |
public long? AssignedBy { get; set; }
|
| 26 |
|
| 27 |
-
public
|
| 28 |
|
| 29 |
[Required]
|
| 30 |
public DateTime DueDate { get; set; }
|
|
|
|
| 24 |
|
| 25 |
public long? AssignedBy { get; set; }
|
| 26 |
|
| 27 |
+
public decimal? EstimatedHours { get; set; }
|
| 28 |
|
| 29 |
[Required]
|
| 30 |
public DateTime DueDate { get; set; }
|
TaskTrackingSystem.WebApi/Features/Project/ProjectService.cs
CHANGED
|
@@ -24,6 +24,8 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 24 |
{
|
| 25 |
return await _db.Projects
|
| 26 |
.Where(p => p.IsDeleted != true)
|
|
|
|
|
|
|
| 27 |
.Select(p => new ProjectDto
|
| 28 |
{
|
| 29 |
Id = p.Id,
|
|
@@ -32,7 +34,8 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 32 |
StartDate = p.StartDate,
|
| 33 |
EndDate = p.EndDate,
|
| 34 |
CreatedById = p.CreatedById,
|
| 35 |
-
CreatedAt = p.CreatedAt ?? DateTime.UtcNow
|
|
|
|
| 36 |
}).ToListAsync();
|
| 37 |
}
|
| 38 |
|
|
@@ -51,7 +54,8 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 51 |
StartDate = project.StartDate,
|
| 52 |
EndDate = project.EndDate,
|
| 53 |
CreatedById = project.CreatedById,
|
| 54 |
-
CreatedAt = project.CreatedAt ?? DateTime.UtcNow
|
|
|
|
| 55 |
};
|
| 56 |
}
|
| 57 |
|
|
@@ -71,7 +75,8 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 71 |
CreatedById = dto.CreatedById,
|
| 72 |
IsDeleted = false,
|
| 73 |
CreatedAt = DateTime.UtcNow,
|
| 74 |
-
CreatedBy = currentUserId
|
|
|
|
| 75 |
};
|
| 76 |
|
| 77 |
_db.Projects.Add(project);
|
|
@@ -85,7 +90,8 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 85 |
StartDate = project.StartDate,
|
| 86 |
EndDate = project.EndDate,
|
| 87 |
CreatedById = project.CreatedById,
|
| 88 |
-
CreatedAt = project.CreatedAt ?? DateTime.UtcNow
|
|
|
|
| 89 |
};
|
| 90 |
|
| 91 |
return Result<ProjectDto>.Success(resultDto, 201);
|
|
@@ -105,6 +111,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 105 |
project.Description = dto.Description;
|
| 106 |
project.StartDate = dto.StartDate;
|
| 107 |
project.EndDate = dto.EndDate;
|
|
|
|
| 108 |
project.UpdatedAt = DateTime.UtcNow;
|
| 109 |
project.UpdatedBy = currentUserId;
|
| 110 |
|
|
@@ -251,7 +258,12 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 251 |
AssignedBy = t.AssignedBy,
|
| 252 |
EstimatedHours = t.EstimatedHours,
|
| 253 |
DueDate = t.DueDate,
|
| 254 |
-
CreatedAt = t.CreatedAt ?? DateTime.UtcNow
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
})
|
| 256 |
.ToListAsync();
|
| 257 |
|
|
|
|
| 24 |
{
|
| 25 |
return await _db.Projects
|
| 26 |
.Where(p => p.IsDeleted != true)
|
| 27 |
+
.OrderBy(p => p.EndDate)
|
| 28 |
+
.ThenByDescending(p => p.CreatedAt ?? DateTime.UtcNow)
|
| 29 |
.Select(p => new ProjectDto
|
| 30 |
{
|
| 31 |
Id = p.Id,
|
|
|
|
| 34 |
StartDate = p.StartDate,
|
| 35 |
EndDate = p.EndDate,
|
| 36 |
CreatedById = p.CreatedById,
|
| 37 |
+
CreatedAt = p.CreatedAt ?? DateTime.UtcNow,
|
| 38 |
+
BudgetedHours = p.BudgetedHours
|
| 39 |
}).ToListAsync();
|
| 40 |
}
|
| 41 |
|
|
|
|
| 54 |
StartDate = project.StartDate,
|
| 55 |
EndDate = project.EndDate,
|
| 56 |
CreatedById = project.CreatedById,
|
| 57 |
+
CreatedAt = project.CreatedAt ?? DateTime.UtcNow,
|
| 58 |
+
BudgetedHours = project.BudgetedHours
|
| 59 |
};
|
| 60 |
}
|
| 61 |
|
|
|
|
| 75 |
CreatedById = dto.CreatedById,
|
| 76 |
IsDeleted = false,
|
| 77 |
CreatedAt = DateTime.UtcNow,
|
| 78 |
+
CreatedBy = currentUserId,
|
| 79 |
+
BudgetedHours = dto.BudgetedHours
|
| 80 |
};
|
| 81 |
|
| 82 |
_db.Projects.Add(project);
|
|
|
|
| 90 |
StartDate = project.StartDate,
|
| 91 |
EndDate = project.EndDate,
|
| 92 |
CreatedById = project.CreatedById,
|
| 93 |
+
CreatedAt = project.CreatedAt ?? DateTime.UtcNow,
|
| 94 |
+
BudgetedHours = project.BudgetedHours
|
| 95 |
};
|
| 96 |
|
| 97 |
return Result<ProjectDto>.Success(resultDto, 201);
|
|
|
|
| 111 |
project.Description = dto.Description;
|
| 112 |
project.StartDate = dto.StartDate;
|
| 113 |
project.EndDate = dto.EndDate;
|
| 114 |
+
project.BudgetedHours = dto.BudgetedHours;
|
| 115 |
project.UpdatedAt = DateTime.UtcNow;
|
| 116 |
project.UpdatedBy = currentUserId;
|
| 117 |
|
|
|
|
| 258 |
AssignedBy = t.AssignedBy,
|
| 259 |
EstimatedHours = t.EstimatedHours,
|
| 260 |
DueDate = t.DueDate,
|
| 261 |
+
CreatedAt = t.CreatedAt ?? DateTime.UtcNow,
|
| 262 |
+
CompletedAt = t.TaskHistories
|
| 263 |
+
.Where(th => th.NewStatusId == 3)
|
| 264 |
+
.OrderBy(th => th.CreatedAt)
|
| 265 |
+
.Select(th => th.CreatedAt)
|
| 266 |
+
.FirstOrDefault()
|
| 267 |
})
|
| 268 |
.ToListAsync();
|
| 269 |
|
TaskTrackingSystem.WebApi/Features/Report/ReportController.cs
CHANGED
|
@@ -110,5 +110,17 @@ namespace TaskTrackingSystem.WebApi.Features.Report
|
|
| 110 |
return File(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 111 |
$"OverdueCriticalTasks_{DateTime.Today:yyyyMMdd}.xlsx");
|
| 112 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
}
|
| 114 |
}
|
|
|
|
| 110 |
return File(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 111 |
$"OverdueCriticalTasks_{DateTime.Today:yyyyMMdd}.xlsx");
|
| 112 |
}
|
| 113 |
+
|
| 114 |
+
// ─── Report 4: Time Tracking ──────────────────────────────────────────────
|
| 115 |
+
|
| 116 |
+
[HttpGet("time-tracking")]
|
| 117 |
+
public async Task<ActionResult<Result<TimeTrackingReportDto>>> GetTimeTracking(
|
| 118 |
+
[FromQuery] string? search,
|
| 119 |
+
[FromQuery] long? projectId,
|
| 120 |
+
[FromQuery] long? statusId)
|
| 121 |
+
{
|
| 122 |
+
var result = await _reportService.GetTimeTrackingReportAsync(search, projectId, statusId);
|
| 123 |
+
return StatusCode(result.StatusCode, result);
|
| 124 |
+
}
|
| 125 |
}
|
| 126 |
}
|
TaskTrackingSystem.WebApi/Features/Report/ReportService.cs
CHANGED
|
@@ -39,12 +39,22 @@ namespace TaskTrackingSystem.WebApi.Features.Report
|
|
| 39 |
if (!string.IsNullOrWhiteSpace(status))
|
| 40 |
{
|
| 41 |
var sl = status.Trim().ToLower();
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
-
var tasks = await query
|
|
|
|
|
|
|
|
|
|
| 48 |
var list = tasks.Select(t => new TaskReportDto
|
| 49 |
{
|
| 50 |
TaskId = t.Id,
|
|
@@ -72,8 +82,21 @@ namespace TaskTrackingSystem.WebApi.Features.Report
|
|
| 72 |
var list = new List<UserProductivityDto>();
|
| 73 |
foreach (var user in users)
|
| 74 |
{
|
| 75 |
-
var tasks = await _db.Tasks
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
list.Add(new UserProductivityDto
|
| 78 |
{
|
| 79 |
UserId = user.Id,
|
|
@@ -81,7 +104,8 @@ namespace TaskTrackingSystem.WebApi.Features.Report
|
|
| 81 |
FullName = $"{user.FirstName} {user.LastName}".Trim(),
|
| 82 |
TotalAssignedTasks = total,
|
| 83 |
CompletedTasksCount = done,
|
| 84 |
-
EfficiencyRatio = total > 0 ? Math.Round(((double)done / total) * 100, 2) : 0
|
|
|
|
| 85 |
});
|
| 86 |
}
|
| 87 |
return Result<IEnumerable<UserProductivityDto>>.Success(list);
|
|
@@ -102,7 +126,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report
|
|
| 102 |
if (projectId.HasValue && projectId > 0)
|
| 103 |
query = query.Where(t => t.ProjectId == projectId.Value);
|
| 104 |
|
| 105 |
-
var tasks = await query.
|
| 106 |
|
| 107 |
if (!string.IsNullOrWhiteSpace(search))
|
| 108 |
{
|
|
@@ -304,5 +328,138 @@ namespace TaskTrackingSystem.WebApi.Features.Report
|
|
| 304 |
wb.SaveAs(ms);
|
| 305 |
return ms.ToArray();
|
| 306 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
}
|
| 308 |
}
|
|
|
|
| 39 |
if (!string.IsNullOrWhiteSpace(status))
|
| 40 |
{
|
| 41 |
var sl = status.Trim().ToLower();
|
| 42 |
+
if (sl == "uncompleted")
|
| 43 |
+
{
|
| 44 |
+
query = query.Where(t => t.StatusId != 3);
|
| 45 |
+
}
|
| 46 |
+
else
|
| 47 |
+
{
|
| 48 |
+
long? sid = sl switch { "to do" => 1, "in progress" => 2, "done" => 3, _ => null };
|
| 49 |
+
if (sid.HasValue) query = query.Where(t => t.StatusId == sid.Value);
|
| 50 |
+
else if (long.TryParse(status, out var pid)) query = query.Where(t => t.StatusId == pid);
|
| 51 |
+
}
|
| 52 |
}
|
| 53 |
|
| 54 |
+
var tasks = await query
|
| 55 |
+
.OrderBy(t => t.DueDate)
|
| 56 |
+
.ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow)
|
| 57 |
+
.ToListAsync();
|
| 58 |
var list = tasks.Select(t => new TaskReportDto
|
| 59 |
{
|
| 60 |
TaskId = t.Id,
|
|
|
|
| 82 |
var list = new List<UserProductivityDto>();
|
| 83 |
foreach (var user in users)
|
| 84 |
{
|
| 85 |
+
var tasks = await _db.Tasks
|
| 86 |
+
.Include(t => t.TaskHistories)
|
| 87 |
+
.Where(t => t.AssignedTo == user.Id && t.IsDeleted != true)
|
| 88 |
+
.ToListAsync();
|
| 89 |
+
|
| 90 |
+
int total = tasks.Count;
|
| 91 |
+
var completedTasks = tasks.Where(t => t.StatusId == 3).ToList();
|
| 92 |
+
int done = completedTasks.Count;
|
| 93 |
+
|
| 94 |
+
int onTimeCount = completedTasks.Count(t =>
|
| 95 |
+
t.TaskHistories.Any(th => th.NewStatusId == 3 && th.CreatedAt <= t.DueDate)
|
| 96 |
+
);
|
| 97 |
+
|
| 98 |
+
double onTimeDeliveryRate = done > 0 ? Math.Round(((double)onTimeCount / done) * 100, 2) : 0;
|
| 99 |
+
|
| 100 |
list.Add(new UserProductivityDto
|
| 101 |
{
|
| 102 |
UserId = user.Id,
|
|
|
|
| 104 |
FullName = $"{user.FirstName} {user.LastName}".Trim(),
|
| 105 |
TotalAssignedTasks = total,
|
| 106 |
CompletedTasksCount = done,
|
| 107 |
+
EfficiencyRatio = total > 0 ? Math.Round(((double)done / total) * 100, 2) : 0,
|
| 108 |
+
OnTimeDeliveryRate = onTimeDeliveryRate
|
| 109 |
});
|
| 110 |
}
|
| 111 |
return Result<IEnumerable<UserProductivityDto>>.Success(list);
|
|
|
|
| 126 |
if (projectId.HasValue && projectId > 0)
|
| 127 |
query = query.Where(t => t.ProjectId == projectId.Value);
|
| 128 |
|
| 129 |
+
var tasks = await query.OrderBy(t => t.DueDate).ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow).ToListAsync();
|
| 130 |
|
| 131 |
if (!string.IsNullOrWhiteSpace(search))
|
| 132 |
{
|
|
|
|
| 328 |
wb.SaveAs(ms);
|
| 329 |
return ms.ToArray();
|
| 330 |
}
|
| 331 |
+
|
| 332 |
+
public async Task<Result<TimeTrackingReportDto>> GetTimeTrackingReportAsync(
|
| 333 |
+
string? search, long? projectId, long? statusId)
|
| 334 |
+
{
|
| 335 |
+
var query = _db.Tasks
|
| 336 |
+
.Include(t => t.Project)
|
| 337 |
+
.Include(t => t.AssignedToNavigation)
|
| 338 |
+
.Include(t => t.TimeLogs.Where(tl => !tl.IsDeleted))
|
| 339 |
+
.Where(t => t.IsDeleted != true);
|
| 340 |
+
|
| 341 |
+
if (projectId.HasValue && projectId > 0)
|
| 342 |
+
query = query.Where(t => t.ProjectId == projectId.Value);
|
| 343 |
+
if (statusId.HasValue && statusId > 0)
|
| 344 |
+
query = query.Where(t => t.StatusId == statusId.Value);
|
| 345 |
+
|
| 346 |
+
var tasks = await query.ToListAsync();
|
| 347 |
+
|
| 348 |
+
if (!string.IsNullOrWhiteSpace(search))
|
| 349 |
+
{
|
| 350 |
+
var s = search.Trim().ToLower();
|
| 351 |
+
tasks = tasks.Where(t =>
|
| 352 |
+
t.Title.ToLower().Contains(s) ||
|
| 353 |
+
(t.Project?.Name.ToLower().Contains(s) == true) ||
|
| 354 |
+
(t.AssignedToNavigation != null &&
|
| 355 |
+
($"{t.AssignedToNavigation.FirstName} {t.AssignedToNavigation.LastName}").ToLower().Contains(s))
|
| 356 |
+
).ToList();
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
var totalEstimatedHrs = tasks.Sum(t => t.EstimatedHours ?? 0m);
|
| 360 |
+
var completedHours = tasks.Sum(t => t.TimeLogs.Sum(tl => tl.HoursLogged));
|
| 361 |
+
var variance = totalEstimatedHrs - completedHours;
|
| 362 |
+
var completionPercentage = totalEstimatedHrs > 0m
|
| 363 |
+
? (double)Math.Round((completedHours / totalEstimatedHrs) * 100m, 2)
|
| 364 |
+
: 0.0;
|
| 365 |
+
|
| 366 |
+
var tasksWithHours = tasks.Count(t => t.EstimatedHours.HasValue || t.TimeLogs.Any());
|
| 367 |
+
var avgHoursPerTask = tasksWithHours > 0
|
| 368 |
+
? Math.Round(totalEstimatedHrs / tasksWithHours, 2)
|
| 369 |
+
: 0m;
|
| 370 |
+
|
| 371 |
+
var result = new TimeTrackingReportDto
|
| 372 |
+
{
|
| 373 |
+
TotalEstimatedHrs = totalEstimatedHrs,
|
| 374 |
+
CompletedHours = completedHours,
|
| 375 |
+
Variance = variance,
|
| 376 |
+
CompletionPercentage = completionPercentage,
|
| 377 |
+
TasksWithHours = tasksWithHours,
|
| 378 |
+
AvgHoursPerTask = avgHoursPerTask
|
| 379 |
+
};
|
| 380 |
+
|
| 381 |
+
// Group by Employee
|
| 382 |
+
var employeeGroups = tasks
|
| 383 |
+
.GroupBy(t => t.AssignedTo)
|
| 384 |
+
.ToList();
|
| 385 |
+
|
| 386 |
+
foreach (var group in employeeGroups)
|
| 387 |
+
{
|
| 388 |
+
var userId = group.Key;
|
| 389 |
+
var user = userId.HasValue ? await _db.Users.FindAsync(userId.Value) : null;
|
| 390 |
+
|
| 391 |
+
var total = group.Count();
|
| 392 |
+
var completed = group.Count(t => t.StatusId == 3);
|
| 393 |
+
var est = group.Sum(t => t.EstimatedHours ?? 0m);
|
| 394 |
+
var comp = group.Sum(t => t.TimeLogs.Sum(tl => tl.HoursLogged));
|
| 395 |
+
var employeeVariance = est - comp;
|
| 396 |
+
var employeePct = est > 0m
|
| 397 |
+
? (double)Math.Round((comp / est) * 100m, 2)
|
| 398 |
+
: 0.0;
|
| 399 |
+
|
| 400 |
+
result.EmployeeSummary.Add(new EmployeeTimeSummaryDto
|
| 401 |
+
{
|
| 402 |
+
UserId = group.Key ?? 0,
|
| 403 |
+
FullName = user != null ? $"{user.FirstName} {user.LastName}".Trim() : "Unassigned",
|
| 404 |
+
Username = user?.Username ?? "unassigned",
|
| 405 |
+
TotalTasks = total,
|
| 406 |
+
CompletedTasks = completed,
|
| 407 |
+
EstHours = est,
|
| 408 |
+
CompletedHours = comp,
|
| 409 |
+
Variance = employeeVariance,
|
| 410 |
+
CompletionPercentage = employeePct
|
| 411 |
+
});
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
// Group by Project
|
| 415 |
+
var projectGroups = tasks
|
| 416 |
+
.GroupBy(t => new { t.ProjectId, ProjectName = t.Project?.Name ?? "Unknown Project" })
|
| 417 |
+
.ToList();
|
| 418 |
+
|
| 419 |
+
foreach (var group in projectGroups)
|
| 420 |
+
{
|
| 421 |
+
var total = group.Count();
|
| 422 |
+
var completed = group.Count(t => t.StatusId == 3);
|
| 423 |
+
var est = group.Sum(t => t.EstimatedHours ?? 0m);
|
| 424 |
+
var comp = group.Sum(t => t.TimeLogs.Sum(tl => tl.HoursLogged));
|
| 425 |
+
var projectVariance = est - comp;
|
| 426 |
+
var projectPct = est > 0m
|
| 427 |
+
? (double)Math.Round((comp / est) * 100m, 2)
|
| 428 |
+
: 0.0;
|
| 429 |
+
|
| 430 |
+
result.ProjectSummary.Add(new ProjectTimeSummaryDto
|
| 431 |
+
{
|
| 432 |
+
ProjectId = group.Key.ProjectId,
|
| 433 |
+
ProjectName = group.Key.ProjectName,
|
| 434 |
+
TotalTasks = total,
|
| 435 |
+
CompletedTasks = completed,
|
| 436 |
+
EstHours = est,
|
| 437 |
+
CompletedHours = comp,
|
| 438 |
+
Variance = projectVariance,
|
| 439 |
+
CompletionPercentage = projectPct
|
| 440 |
+
});
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
// Task Details
|
| 444 |
+
foreach (var t in tasks)
|
| 445 |
+
{
|
| 446 |
+
result.TaskDetail.Add(new TaskTimeDetailDto
|
| 447 |
+
{
|
| 448 |
+
TaskId = t.Id,
|
| 449 |
+
Title = t.Title,
|
| 450 |
+
ProjectId = t.ProjectId,
|
| 451 |
+
ProjectName = t.Project?.Name ?? "",
|
| 452 |
+
AssignedTo = t.AssignedTo,
|
| 453 |
+
AssigneeName = t.AssignedToNavigation != null ? $"{t.AssignedToNavigation.FirstName} {t.AssignedToNavigation.LastName}".Trim() : "Unassigned",
|
| 454 |
+
AssigneeUsername = t.AssignedToNavigation?.Username ?? "",
|
| 455 |
+
EstimatedHours = t.EstimatedHours,
|
| 456 |
+
CompletedHours = t.TimeLogs.Sum(tl => tl.HoursLogged),
|
| 457 |
+
DueDate = t.DueDate,
|
| 458 |
+
StatusId = t.StatusId
|
| 459 |
+
});
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
return Result<TimeTrackingReportDto>.Success(result);
|
| 463 |
+
}
|
| 464 |
}
|
| 465 |
}
|
TaskTrackingSystem.WebApi/Features/Role/RoleController.cs
CHANGED
|
@@ -58,27 +58,6 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 58 |
return StatusCode(result.StatusCode, result);
|
| 59 |
}
|
| 60 |
|
| 61 |
-
[HttpPost("{id}/permissions")]
|
| 62 |
-
public async Task<IActionResult> AssignPermissions(long id, [FromBody] AssignPermissionsDto dto)
|
| 63 |
-
{
|
| 64 |
-
var result = await _roleService.AssignPermissionsToRoleAsync(id, dto);
|
| 65 |
-
if (!result.IsSuccess)
|
| 66 |
-
{
|
| 67 |
-
return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
|
| 68 |
-
}
|
| 69 |
-
return Ok();
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
[HttpGet("{id}/permissions")]
|
| 73 |
-
public async Task<ActionResult<Result<List<long>>>> GetAssignedPermissions(long id)
|
| 74 |
-
{
|
| 75 |
-
var result = await _roleService.GetAssignedPermissionsByRoleIdAsync(id);
|
| 76 |
-
if (!result.IsSuccess)
|
| 77 |
-
{
|
| 78 |
-
return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
|
| 79 |
-
}
|
| 80 |
-
return Ok(result);
|
| 81 |
-
}
|
| 82 |
|
| 83 |
[HttpGet("{id}/menus")]
|
| 84 |
public async Task<ActionResult<Result<List<string>>>> GetAssignedMenus(long id)
|
|
|
|
| 58 |
return StatusCode(result.StatusCode, result);
|
| 59 |
}
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
[HttpGet("{id}/menus")]
|
| 63 |
public async Task<ActionResult<Result<List<string>>>> GetAssignedMenus(long id)
|
TaskTrackingSystem.WebApi/Features/Role/RoleService.cs
CHANGED
|
@@ -137,74 +137,6 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 137 |
return Result.Success(200);
|
| 138 |
}
|
| 139 |
|
| 140 |
-
public async Task<Result> AssignPermissionsToRoleAsync(long roleId, AssignPermissionsDto dto)
|
| 141 |
-
{
|
| 142 |
-
var role = await _db.Roles.FirstOrDefaultAsync(r => r.Id == roleId && r.IsDeleted != true);
|
| 143 |
-
if (role == null)
|
| 144 |
-
{
|
| 145 |
-
return Result.Failure(ResultMessages.RoleNotFound(roleId), 404);
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
if (dto.PermissionIds == null)
|
| 149 |
-
{
|
| 150 |
-
return Result.Failure(ResultMessages.PermissionIdsCannotBeNull, 400);
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
// Verify that all requested permission IDs are valid (not deleted)
|
| 154 |
-
if (dto.PermissionIds.Any())
|
| 155 |
-
{
|
| 156 |
-
var validPermissionIds = await _db.Permissions
|
| 157 |
-
.Where(p => dto.PermissionIds.Contains(p.Id) && p.IsDeleted != true)
|
| 158 |
-
.Select(p => p.Id)
|
| 159 |
-
.ToListAsync();
|
| 160 |
-
|
| 161 |
-
var invalidPermissionIds = dto.PermissionIds.Except(validPermissionIds).ToList();
|
| 162 |
-
if (invalidPermissionIds.Any())
|
| 163 |
-
{
|
| 164 |
-
return Result.Failure(ResultMessages.InvalidPermissionIds(string.Join(", ", invalidPermissionIds)), 400);
|
| 165 |
-
}
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
// Remove existing role permissions matching roleId
|
| 169 |
-
var existingRolePermissions = await _db.RolePermissions
|
| 170 |
-
.Where(rp => rp.RoleId == roleId)
|
| 171 |
-
.ToListAsync();
|
| 172 |
-
|
| 173 |
-
if (existingRolePermissions.Any())
|
| 174 |
-
{
|
| 175 |
-
_db.RolePermissions.RemoveRange(existingRolePermissions);
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
// Bulk insert new RolePermission links
|
| 179 |
-
foreach (var permissionId in dto.PermissionIds)
|
| 180 |
-
{
|
| 181 |
-
_db.RolePermissions.Add(new RolePermission
|
| 182 |
-
{
|
| 183 |
-
RoleId = roleId,
|
| 184 |
-
PermissionId = permissionId,
|
| 185 |
-
CreatedAt = DateTime.UtcNow
|
| 186 |
-
});
|
| 187 |
-
}
|
| 188 |
-
|
| 189 |
-
await _db.SaveChangesAsync();
|
| 190 |
-
return Result.Success(200);
|
| 191 |
-
}
|
| 192 |
-
|
| 193 |
-
public async Task<Result<List<long>>> GetAssignedPermissionsByRoleIdAsync(long roleId)
|
| 194 |
-
{
|
| 195 |
-
var roleExists = await _db.Roles.AnyAsync(r => r.Id == roleId && r.IsDeleted != true);
|
| 196 |
-
if (!roleExists)
|
| 197 |
-
{
|
| 198 |
-
return Result<List<long>>.Failure(ResultMessages.RoleNotFound(roleId), 404);
|
| 199 |
-
}
|
| 200 |
-
|
| 201 |
-
var permissionIds = await _db.RolePermissions
|
| 202 |
-
.Where(rp => rp.RoleId == roleId)
|
| 203 |
-
.Select(rp => rp.PermissionId)
|
| 204 |
-
.ToListAsync();
|
| 205 |
-
|
| 206 |
-
return Result<List<long>>.Success(permissionIds);
|
| 207 |
-
}
|
| 208 |
|
| 209 |
public async Task<Result<List<string>>> GetAssignedMenusByRoleIdAsync(long roleId)
|
| 210 |
{
|
|
|
|
| 137 |
return Result.Success(200);
|
| 138 |
}
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
public async Task<Result<List<string>>> GetAssignedMenusByRoleIdAsync(long roleId)
|
| 142 |
{
|
TaskTrackingSystem.WebApi/Features/Task/TaskService.cs
CHANGED
|
@@ -24,6 +24,8 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 24 |
{
|
| 25 |
return await _db.Tasks
|
| 26 |
.Where(t => t.IsDeleted != true)
|
|
|
|
|
|
|
| 27 |
.Select(t => new TaskDto
|
| 28 |
{
|
| 29 |
Id = t.Id,
|
|
@@ -36,13 +38,19 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 36 |
AssignedBy = t.AssignedBy,
|
| 37 |
EstimatedHours = t.EstimatedHours,
|
| 38 |
DueDate = t.DueDate,
|
| 39 |
-
CreatedAt = t.CreatedAt ?? DateTime.UtcNow
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
}).ToListAsync();
|
| 41 |
}
|
| 42 |
|
| 43 |
public async Task<TaskDto?> GetTaskByIdAsync(long id)
|
| 44 |
{
|
| 45 |
var task = await _db.Tasks
|
|
|
|
| 46 |
.FirstOrDefaultAsync(t => t.Id == id && t.IsDeleted != true);
|
| 47 |
|
| 48 |
if (task == null) return null;
|
|
@@ -59,7 +67,12 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 59 |
AssignedBy = task.AssignedBy,
|
| 60 |
EstimatedHours = task.EstimatedHours,
|
| 61 |
DueDate = task.DueDate,
|
| 62 |
-
CreatedAt = task.CreatedAt ?? DateTime.UtcNow
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
};
|
| 64 |
}
|
| 65 |
|
|
@@ -111,7 +124,8 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 111 |
AssignedBy = task.AssignedBy,
|
| 112 |
EstimatedHours = task.EstimatedHours,
|
| 113 |
DueDate = task.DueDate,
|
| 114 |
-
CreatedAt = task.CreatedAt ?? DateTime.UtcNow
|
|
|
|
| 115 |
};
|
| 116 |
|
| 117 |
return Result<TaskDto>.Success(resultDto, 201);
|
|
@@ -176,7 +190,12 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 176 |
AssignedBy = t.AssignedBy,
|
| 177 |
EstimatedHours = t.EstimatedHours,
|
| 178 |
DueDate = t.DueDate,
|
| 179 |
-
CreatedAt = t.CreatedAt ?? DateTime.UtcNow
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
})
|
| 181 |
.ToListAsync();
|
| 182 |
|
|
|
|
| 24 |
{
|
| 25 |
return await _db.Tasks
|
| 26 |
.Where(t => t.IsDeleted != true)
|
| 27 |
+
.OrderBy(t => t.DueDate)
|
| 28 |
+
.ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow)
|
| 29 |
.Select(t => new TaskDto
|
| 30 |
{
|
| 31 |
Id = t.Id,
|
|
|
|
| 38 |
AssignedBy = t.AssignedBy,
|
| 39 |
EstimatedHours = t.EstimatedHours,
|
| 40 |
DueDate = t.DueDate,
|
| 41 |
+
CreatedAt = t.CreatedAt ?? DateTime.UtcNow,
|
| 42 |
+
CompletedAt = t.TaskHistories
|
| 43 |
+
.Where(th => th.NewStatusId == 3)
|
| 44 |
+
.OrderBy(th => th.CreatedAt)
|
| 45 |
+
.Select(th => th.CreatedAt)
|
| 46 |
+
.FirstOrDefault()
|
| 47 |
}).ToListAsync();
|
| 48 |
}
|
| 49 |
|
| 50 |
public async Task<TaskDto?> GetTaskByIdAsync(long id)
|
| 51 |
{
|
| 52 |
var task = await _db.Tasks
|
| 53 |
+
.Include(t => t.TaskHistories)
|
| 54 |
.FirstOrDefaultAsync(t => t.Id == id && t.IsDeleted != true);
|
| 55 |
|
| 56 |
if (task == null) return null;
|
|
|
|
| 67 |
AssignedBy = task.AssignedBy,
|
| 68 |
EstimatedHours = task.EstimatedHours,
|
| 69 |
DueDate = task.DueDate,
|
| 70 |
+
CreatedAt = task.CreatedAt ?? DateTime.UtcNow,
|
| 71 |
+
CompletedAt = task.TaskHistories
|
| 72 |
+
.Where(th => th.NewStatusId == 3)
|
| 73 |
+
.OrderBy(th => th.CreatedAt)
|
| 74 |
+
.Select(th => th.CreatedAt)
|
| 75 |
+
.FirstOrDefault()
|
| 76 |
};
|
| 77 |
}
|
| 78 |
|
|
|
|
| 124 |
AssignedBy = task.AssignedBy,
|
| 125 |
EstimatedHours = task.EstimatedHours,
|
| 126 |
DueDate = task.DueDate,
|
| 127 |
+
CreatedAt = task.CreatedAt ?? DateTime.UtcNow,
|
| 128 |
+
CompletedAt = null
|
| 129 |
};
|
| 130 |
|
| 131 |
return Result<TaskDto>.Success(resultDto, 201);
|
|
|
|
| 190 |
AssignedBy = t.AssignedBy,
|
| 191 |
EstimatedHours = t.EstimatedHours,
|
| 192 |
DueDate = t.DueDate,
|
| 193 |
+
CreatedAt = t.CreatedAt ?? DateTime.UtcNow,
|
| 194 |
+
CompletedAt = t.TaskHistories
|
| 195 |
+
.Where(th => th.NewStatusId == 3)
|
| 196 |
+
.OrderBy(th => th.CreatedAt)
|
| 197 |
+
.Select(th => th.CreatedAt)
|
| 198 |
+
.FirstOrDefault()
|
| 199 |
})
|
| 200 |
.ToListAsync();
|
| 201 |
|
TaskTrackingSystem.WebApp/Components/Pages/EmployeeReport.razor
CHANGED
|
@@ -132,6 +132,7 @@
|
|
| 132 |
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Total Assigned</th>
|
| 133 |
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Completed Tasks</th>
|
| 134 |
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Efficiency</th>
|
|
|
|
| 135 |
</tr>
|
| 136 |
</thead>
|
| 137 |
<tbody class="divide-y divide-slate-100">
|
|
@@ -161,13 +162,24 @@
|
|
| 161 |
<span class="text-sm font-semibold text-slate-700">@emp.Efficiency.ToString("F0")%</span>
|
| 162 |
</div>
|
| 163 |
</td>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
</tr>
|
| 165 |
}
|
| 166 |
}
|
| 167 |
else
|
| 168 |
{
|
| 169 |
<tr>
|
| 170 |
-
<td colspan="
|
| 171 |
<p class="text-sm text-slate-400">No employee data found matching criteria.</p>
|
| 172 |
</td>
|
| 173 |
</tr>
|
|
@@ -331,13 +343,18 @@
|
|
| 331 |
int completedCount = userTasks.Count(t => t.StatusId == 3); // Done = 3
|
| 332 |
double efficiency = assignedCount > 0 ? ((double)completedCount / assignedCount) * 100 : 0;
|
| 333 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
metrics.Add(new EmployeeMetric
|
| 335 |
{
|
| 336 |
FullName = $"{u.FirstName} {u.LastName}".Trim(),
|
| 337 |
Username = u.Username,
|
| 338 |
AssignedCount = assignedCount,
|
| 339 |
CompletedCount = completedCount,
|
| 340 |
-
Efficiency = efficiency
|
|
|
|
| 341 |
});
|
| 342 |
}
|
| 343 |
|
|
@@ -347,11 +364,11 @@
|
|
| 347 |
private async Task DownloadCsv()
|
| 348 |
{
|
| 349 |
var csv = new StringBuilder();
|
| 350 |
-
csv.AppendLine("Employee Name,Username,Total Assigned,Completed Tasks,Efficiency %");
|
| 351 |
|
| 352 |
foreach (var emp in displayList)
|
| 353 |
{
|
| 354 |
-
csv.AppendLine($"\"{emp.FullName}\",\"{emp.Username}\",\"{emp.AssignedCount}\",\"{emp.CompletedCount}\",\"{emp.Efficiency:F0}%\"");
|
| 355 |
}
|
| 356 |
|
| 357 |
await JS.InvokeVoidAsync("downloadFile", "EmployeeProductivityReport.csv", "text/csv;charset=utf-8;", csv.ToString());
|
|
@@ -364,5 +381,6 @@
|
|
| 364 |
public int AssignedCount { get; set; }
|
| 365 |
public int CompletedCount { get; set; }
|
| 366 |
public double Efficiency { get; set; }
|
|
|
|
| 367 |
}
|
| 368 |
}
|
|
|
|
| 132 |
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Total Assigned</th>
|
| 133 |
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Completed Tasks</th>
|
| 134 |
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Efficiency</th>
|
| 135 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">On-Time Delivery</th>
|
| 136 |
</tr>
|
| 137 |
</thead>
|
| 138 |
<tbody class="divide-y divide-slate-100">
|
|
|
|
| 162 |
<span class="text-sm font-semibold text-slate-700">@emp.Efficiency.ToString("F0")%</span>
|
| 163 |
</div>
|
| 164 |
</td>
|
| 165 |
+
<td class="px-6 py-4">
|
| 166 |
+
<div class="flex items-center space-x-3">
|
| 167 |
+
<div class="flex-1 h-2.5 rounded-full bg-slate-100 overflow-hidden max-w-[120px]">
|
| 168 |
+
<div class="h-full rounded-full @(emp.OnTimeDeliveryRate >= 80 ? "bg-emerald-500" : emp.OnTimeDeliveryRate >= 50 ? "bg-amber-400" : "bg-rose-500") transition-all"
|
| 169 |
+
style="width: @(Math.Min(emp.OnTimeDeliveryRate, 100))%"></div>
|
| 170 |
+
</div>
|
| 171 |
+
<span class="text-sm font-semibold @(emp.OnTimeDeliveryRate >= 80 ? "text-emerald-600" : emp.OnTimeDeliveryRate >= 50 ? "text-amber-500" : "text-rose-600")">
|
| 172 |
+
@emp.OnTimeDeliveryRate.ToString("F0")%
|
| 173 |
+
</span>
|
| 174 |
+
</div>
|
| 175 |
+
</td>
|
| 176 |
</tr>
|
| 177 |
}
|
| 178 |
}
|
| 179 |
else
|
| 180 |
{
|
| 181 |
<tr>
|
| 182 |
+
<td colspan="5" class="px-6 py-12 text-center">
|
| 183 |
<p class="text-sm text-slate-400">No employee data found matching criteria.</p>
|
| 184 |
</td>
|
| 185 |
</tr>
|
|
|
|
| 343 |
int completedCount = userTasks.Count(t => t.StatusId == 3); // Done = 3
|
| 344 |
double efficiency = assignedCount > 0 ? ((double)completedCount / assignedCount) * 100 : 0;
|
| 345 |
|
| 346 |
+
var completedTasksList = userTasks.Where(t => t.StatusId == 3).ToList();
|
| 347 |
+
int onTimeCompletedCount = completedTasksList.Count(t => t.CompletedAt.HasValue && t.CompletedAt.Value.Date <= t.DueDate.Date);
|
| 348 |
+
double onTimeDeliveryRate = completedCount > 0 ? ((double)onTimeCompletedCount / completedCount) * 100 : 0;
|
| 349 |
+
|
| 350 |
metrics.Add(new EmployeeMetric
|
| 351 |
{
|
| 352 |
FullName = $"{u.FirstName} {u.LastName}".Trim(),
|
| 353 |
Username = u.Username,
|
| 354 |
AssignedCount = assignedCount,
|
| 355 |
CompletedCount = completedCount,
|
| 356 |
+
Efficiency = efficiency,
|
| 357 |
+
OnTimeDeliveryRate = onTimeDeliveryRate
|
| 358 |
});
|
| 359 |
}
|
| 360 |
|
|
|
|
| 364 |
private async Task DownloadCsv()
|
| 365 |
{
|
| 366 |
var csv = new StringBuilder();
|
| 367 |
+
csv.AppendLine("Employee Name,Username,Total Assigned,Completed Tasks,Efficiency %,On-Time Delivery %");
|
| 368 |
|
| 369 |
foreach (var emp in displayList)
|
| 370 |
{
|
| 371 |
+
csv.AppendLine($"\"{emp.FullName}\",\"{emp.Username}\",\"{emp.AssignedCount}\",\"{emp.CompletedCount}\",\"{emp.Efficiency:F0}%\",\"{emp.OnTimeDeliveryRate:F0}%\"");
|
| 372 |
}
|
| 373 |
|
| 374 |
await JS.InvokeVoidAsync("downloadFile", "EmployeeProductivityReport.csv", "text/csv;charset=utf-8;", csv.ToString());
|
|
|
|
| 381 |
public int AssignedCount { get; set; }
|
| 382 |
public int CompletedCount { get; set; }
|
| 383 |
public double Efficiency { get; set; }
|
| 384 |
+
public double OnTimeDeliveryRate { get; set; }
|
| 385 |
}
|
| 386 |
}
|
TaskTrackingSystem.WebApp/Components/Pages/OverdueTasksReport.razor
CHANGED
|
@@ -133,6 +133,7 @@
|
|
| 133 |
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Days Overdue</th>
|
| 134 |
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Priority</th>
|
| 135 |
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
|
|
|
|
| 136 |
</tr>
|
| 137 |
</thead>
|
| 138 |
<tbody class="divide-y divide-slate-100">
|
|
@@ -216,6 +217,14 @@
|
|
| 216 |
}
|
| 217 |
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @statusClass">@statusLabel</span>
|
| 218 |
</td>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
</tr>
|
| 220 |
}
|
| 221 |
}
|
|
@@ -235,6 +244,41 @@
|
|
| 235 |
OnPageSizeChanged="HandlePageSizeChanged" />
|
| 236 |
</div>
|
| 237 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
</div>
|
| 239 |
|
| 240 |
@code {
|
|
@@ -378,4 +422,80 @@
|
|
| 378 |
var base64 = Convert.ToBase64String(bytes);
|
| 379 |
await JS.InvokeVoidAsync("downloadFile", $"overdue_tasks_{DateTime.Today:yyyy-MM-dd}.csv", "text/csv", base64);
|
| 380 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
}
|
|
|
|
| 133 |
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Days Overdue</th>
|
| 134 |
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Priority</th>
|
| 135 |
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
|
| 136 |
+
<th class="px-6 py-3 text-right text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
|
| 137 |
</tr>
|
| 138 |
</thead>
|
| 139 |
<tbody class="divide-y divide-slate-100">
|
|
|
|
| 217 |
}
|
| 218 |
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @statusClass">@statusLabel</span>
|
| 219 |
</td>
|
| 220 |
+
<td class="px-6 py-4 text-right space-x-3 whitespace-nowrap">
|
| 221 |
+
<button @onclick="() => RemindAssignee(task)" class="text-xs font-semibold text-violet-600 hover:text-violet-900 transition-colors font-sans">
|
| 222 |
+
Remind
|
| 223 |
+
</button>
|
| 224 |
+
<button @onclick="() => OpenAdjustDueDateModal(task)" class="text-xs font-semibold text-slate-600 hover:text-slate-900 transition-colors font-sans">
|
| 225 |
+
Adjust Due Date
|
| 226 |
+
</button>
|
| 227 |
+
</td>
|
| 228 |
</tr>
|
| 229 |
}
|
| 230 |
}
|
|
|
|
| 244 |
OnPageSizeChanged="HandlePageSizeChanged" />
|
| 245 |
</div>
|
| 246 |
}
|
| 247 |
+
|
| 248 |
+
@if (isAdjustModalOpen && selectedTaskForAdjustment != null)
|
| 249 |
+
{
|
| 250 |
+
<div class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 backdrop-blur-sm transition-opacity">
|
| 251 |
+
<div class="relative w-full max-w-md rounded-2xl bg-white p-6 shadow-xl border border-slate-100 animate-in fade-in zoom-in duration-200">
|
| 252 |
+
<h3 class="text-lg font-bold text-slate-900 mb-2 font-sans">Adjust Due Date</h3>
|
| 253 |
+
<p class="text-sm text-slate-500 mb-4 font-sans">Update the due date for task: <span class="font-semibold text-slate-700">@selectedTaskForAdjustment.Title</span></p>
|
| 254 |
+
|
| 255 |
+
<div class="space-y-4">
|
| 256 |
+
<div>
|
| 257 |
+
<label class="block text-xs font-medium text-slate-500 mb-1.5 font-sans">New Due Date</label>
|
| 258 |
+
<input @bind="adjustedDueDate" type="date" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
|
| 259 |
+
</div>
|
| 260 |
+
|
| 261 |
+
@if (!string.IsNullOrEmpty(adjustError))
|
| 262 |
+
{
|
| 263 |
+
<div class="rounded-lg bg-rose-50 p-3 text-xs font-medium text-rose-700 font-sans">@adjustError</div>
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
<div class="flex items-center justify-end space-x-3 pt-2">
|
| 267 |
+
<button @onclick="CloseAdjustModal" class="rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors font-sans">
|
| 268 |
+
Cancel
|
| 269 |
+
</button>
|
| 270 |
+
<button @onclick="SaveDueDateAdjustment" disabled="@isAdjusting" class="rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors disabled:opacity-50 shadow-sm flex items-center font-sans">
|
| 271 |
+
@if (isAdjusting)
|
| 272 |
+
{
|
| 273 |
+
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
|
| 274 |
+
}
|
| 275 |
+
Save Changes
|
| 276 |
+
</button>
|
| 277 |
+
</div>
|
| 278 |
+
</div>
|
| 279 |
+
</div>
|
| 280 |
+
</div>
|
| 281 |
+
}
|
| 282 |
</div>
|
| 283 |
|
| 284 |
@code {
|
|
|
|
| 422 |
var base64 = Convert.ToBase64String(bytes);
|
| 423 |
await JS.InvokeVoidAsync("downloadFile", $"overdue_tasks_{DateTime.Today:yyyy-MM-dd}.csv", "text/csv", base64);
|
| 424 |
}
|
| 425 |
+
|
| 426 |
+
// Adjust Due Date modal variables and methods
|
| 427 |
+
private bool isAdjustModalOpen = false;
|
| 428 |
+
private TaskDto? selectedTaskForAdjustment;
|
| 429 |
+
private DateTime adjustedDueDate = DateTime.Today;
|
| 430 |
+
private string adjustError = string.Empty;
|
| 431 |
+
private bool isAdjusting = false;
|
| 432 |
+
|
| 433 |
+
private void RemindAssignee(TaskDto task)
|
| 434 |
+
{
|
| 435 |
+
var assigneeName = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo) is { } usr ? $"{usr.FirstName} {usr.LastName}" : "Unassigned";
|
| 436 |
+
Console.WriteLine($"[REMINDER PLACEHOLDER] Sent reminder notification to assignee '{assigneeName}' for overdue task ID {task.Id} ('{task.Title}')");
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
private void OpenAdjustDueDateModal(TaskDto task)
|
| 440 |
+
{
|
| 441 |
+
selectedTaskForAdjustment = task;
|
| 442 |
+
adjustedDueDate = task.DueDate;
|
| 443 |
+
adjustError = string.Empty;
|
| 444 |
+
isAdjustModalOpen = true;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
private void CloseAdjustModal()
|
| 448 |
+
{
|
| 449 |
+
isAdjustModalOpen = false;
|
| 450 |
+
selectedTaskForAdjustment = null;
|
| 451 |
+
adjustError = string.Empty;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
private async Task SaveDueDateAdjustment()
|
| 455 |
+
{
|
| 456 |
+
if (selectedTaskForAdjustment == null) return;
|
| 457 |
+
|
| 458 |
+
isAdjusting = true;
|
| 459 |
+
adjustError = string.Empty;
|
| 460 |
+
var client = ApiClient.CreateClient();
|
| 461 |
+
try
|
| 462 |
+
{
|
| 463 |
+
var updateDto = new UpdateTaskDto
|
| 464 |
+
{
|
| 465 |
+
Title = selectedTaskForAdjustment.Title,
|
| 466 |
+
Description = selectedTaskForAdjustment.Description,
|
| 467 |
+
StatusId = selectedTaskForAdjustment.StatusId,
|
| 468 |
+
PriorityId = selectedTaskForAdjustment.PriorityId,
|
| 469 |
+
AssignedTo = selectedTaskForAdjustment.AssignedTo,
|
| 470 |
+
AssignedBy = selectedTaskForAdjustment.AssignedBy,
|
| 471 |
+
EstimatedHours = selectedTaskForAdjustment.EstimatedHours,
|
| 472 |
+
DueDate = adjustedDueDate
|
| 473 |
+
};
|
| 474 |
+
|
| 475 |
+
var response = await client.PutAsJsonAsync($"Task/{selectedTaskForAdjustment.Id}", updateDto);
|
| 476 |
+
if (response.IsSuccessStatusCode)
|
| 477 |
+
{
|
| 478 |
+
var t = allTasks.FirstOrDefault(x => x.Id == selectedTaskForAdjustment.Id);
|
| 479 |
+
if (t != null)
|
| 480 |
+
{
|
| 481 |
+
t.DueDate = adjustedDueDate;
|
| 482 |
+
}
|
| 483 |
+
ComputeSummary();
|
| 484 |
+
CloseAdjustModal();
|
| 485 |
+
}
|
| 486 |
+
else
|
| 487 |
+
{
|
| 488 |
+
var errorRes = await response.Content.ReadFromJsonAsync<Result>();
|
| 489 |
+
adjustError = errorRes?.ErrorMessage ?? "Failed to update due date. Please try again.";
|
| 490 |
+
}
|
| 491 |
+
}
|
| 492 |
+
catch (Exception ex)
|
| 493 |
+
{
|
| 494 |
+
adjustError = $"Error: {ex.Message}";
|
| 495 |
+
}
|
| 496 |
+
finally
|
| 497 |
+
{
|
| 498 |
+
isAdjusting = false;
|
| 499 |
+
}
|
| 500 |
+
}
|
| 501 |
}
|
TaskTrackingSystem.WebApp/Components/Pages/ProjectAssign.razor
CHANGED
|
@@ -218,6 +218,7 @@
|
|
| 218 |
private List<ProjectDto> projects = new();
|
| 219 |
private List<UserDto> allUsers = new();
|
| 220 |
private List<RoleDto> roles = new();
|
|
|
|
| 221 |
|
| 222 |
private ProjectDto? SelectedProject { get; set; }
|
| 223 |
private HashSet<long> SelectedUserIds { get; set; } = new();
|
|
@@ -240,7 +241,11 @@
|
|
| 240 |
|
| 241 |
projects = projectsTask.Result ?? new();
|
| 242 |
allUsers = usersTask.Result ?? new();
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
}
|
| 245 |
catch (Exception ex)
|
| 246 |
{
|
|
@@ -267,6 +272,10 @@
|
|
| 267 |
get
|
| 268 |
{
|
| 269 |
var result = allUsers.Where(u => u.IsActive);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
if (selectedRoleId > 0)
|
| 271 |
{
|
| 272 |
result = result.Where(u => u.RoleId == selectedRoleId);
|
|
|
|
| 218 |
private List<ProjectDto> projects = new();
|
| 219 |
private List<UserDto> allUsers = new();
|
| 220 |
private List<RoleDto> roles = new();
|
| 221 |
+
private long? adminRoleId;
|
| 222 |
|
| 223 |
private ProjectDto? SelectedProject { get; set; }
|
| 224 |
private HashSet<long> SelectedUserIds { get; set; } = new();
|
|
|
|
| 241 |
|
| 242 |
projects = projectsTask.Result ?? new();
|
| 243 |
allUsers = usersTask.Result ?? new();
|
| 244 |
+
|
| 245 |
+
var allLoadedRoles = rolesTask.Result ?? new();
|
| 246 |
+
var adminRole = allLoadedRoles.FirstOrDefault(r => string.Equals(r.Name, "Admin", StringComparison.OrdinalIgnoreCase));
|
| 247 |
+
adminRoleId = adminRole?.Id;
|
| 248 |
+
roles = allLoadedRoles.Where(r => !string.Equals(r.Name, "Admin", StringComparison.OrdinalIgnoreCase)).ToList();
|
| 249 |
}
|
| 250 |
catch (Exception ex)
|
| 251 |
{
|
|
|
|
| 272 |
get
|
| 273 |
{
|
| 274 |
var result = allUsers.Where(u => u.IsActive);
|
| 275 |
+
if (adminRoleId.HasValue)
|
| 276 |
+
{
|
| 277 |
+
result = result.Where(u => u.RoleId != adminRoleId.Value);
|
| 278 |
+
}
|
| 279 |
if (selectedRoleId > 0)
|
| 280 |
{
|
| 281 |
result = result.Where(u => u.RoleId == selectedRoleId);
|
TaskTrackingSystem.WebApp/Components/Pages/ProjectProgressReport.razor
CHANGED
|
@@ -95,6 +95,45 @@
|
|
| 95 |
}
|
| 96 |
else
|
| 97 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
@if (isChartView)
|
| 99 |
{
|
| 100 |
<!-- Chart View: SVG Project Progress Bars -->
|
|
@@ -202,6 +241,11 @@
|
|
| 202 |
private int currentPage = 1;
|
| 203 |
private int pageSize = 10;
|
| 204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
private void ApplyAllFilters()
|
| 206 |
{
|
| 207 |
ApplyPeriodPreset();
|
|
@@ -308,6 +352,8 @@
|
|
| 308 |
filteredProjects = filteredProjects.Where(p => p.StartDate <= filterEndDate.Value.AddDays(1)).ToList();
|
| 309 |
}
|
| 310 |
|
|
|
|
|
|
|
| 311 |
foreach (var p in filteredProjects)
|
| 312 |
{
|
| 313 |
var projectTasks = tasks.Where(t => t.ProjectId == p.Id).ToList();
|
|
@@ -315,7 +361,7 @@
|
|
| 315 |
// Apply filter status on tasks if matching
|
| 316 |
if (filterStatus == "overdue")
|
| 317 |
{
|
| 318 |
-
projectTasks = projectTasks.Where(t => t.DueDate.Date <
|
| 319 |
}
|
| 320 |
else if (long.TryParse(filterStatus, out var statusId) && statusId > 0)
|
| 321 |
{
|
|
@@ -326,6 +372,21 @@
|
|
| 326 |
int completedTasks = projectTasks.Count(t => t.StatusId == 3); // Done = 3
|
| 327 |
double progress = totalTasks > 0 ? ((double)completedTasks / totalTasks) * 100 : 0;
|
| 328 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
metrics.Add(new ProjectMetric
|
| 330 |
{
|
| 331 |
ProjectName = p.Name,
|
|
@@ -333,11 +394,18 @@
|
|
| 333 |
EndDate = p.EndDate,
|
| 334 |
TotalTasks = totalTasks,
|
| 335 |
CompletedTasks = completedTasks,
|
| 336 |
-
Progress = progress
|
|
|
|
|
|
|
| 337 |
});
|
| 338 |
}
|
| 339 |
|
| 340 |
displayList = metrics.OrderByDescending(m => m.Progress).ToList();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
}
|
| 342 |
|
| 343 |
private async Task DownloadCsv()
|
|
@@ -361,5 +429,7 @@
|
|
| 361 |
public int TotalTasks { get; set; }
|
| 362 |
public int CompletedTasks { get; set; }
|
| 363 |
public double Progress { get; set; }
|
|
|
|
|
|
|
| 364 |
}
|
| 365 |
}
|
|
|
|
| 95 |
}
|
| 96 |
else
|
| 97 |
{
|
| 98 |
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
| 99 |
+
<!-- Total Active Projects Card -->
|
| 100 |
+
<div class="rounded-xl border border-violet-100 bg-violet-50 p-5 shadow-sm">
|
| 101 |
+
<div class="flex items-center justify-between">
|
| 102 |
+
<div>
|
| 103 |
+
<p class="text-xs font-semibold uppercase tracking-wider text-violet-500 font-sans">Total Active Projects</p>
|
| 104 |
+
<p class="mt-1 text-3xl font-bold text-violet-700">@totalActiveProjects</p>
|
| 105 |
+
</div>
|
| 106 |
+
<span class="flex h-11 w-11 items-center justify-center rounded-xl bg-violet-100">
|
| 107 |
+
<i data-lucide="folder" class="h-6 w-6 text-violet-600"></i>
|
| 108 |
+
</span>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
<!-- Ahead of Schedule Card -->
|
| 112 |
+
<div class="rounded-xl border border-emerald-100 bg-emerald-50 p-5 shadow-sm">
|
| 113 |
+
<div class="flex items-center justify-between">
|
| 114 |
+
<div>
|
| 115 |
+
<p class="text-xs font-semibold uppercase tracking-wider text-emerald-500 font-sans">Ahead of Schedule</p>
|
| 116 |
+
<p class="mt-1 text-3xl font-bold text-emerald-700">@aheadOfScheduleCount</p>
|
| 117 |
+
</div>
|
| 118 |
+
<span class="flex h-11 w-11 items-center justify-center rounded-xl bg-emerald-100">
|
| 119 |
+
<i data-lucide="trending-up" class="h-6 w-6 text-emerald-600"></i>
|
| 120 |
+
</span>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
<!-- At Risk Card -->
|
| 124 |
+
<div class="rounded-xl border border-rose-100 bg-rose-50 p-5 shadow-sm">
|
| 125 |
+
<div class="flex items-center justify-between">
|
| 126 |
+
<div>
|
| 127 |
+
<p class="text-xs font-semibold uppercase tracking-wider text-rose-500 font-sans">At Risk</p>
|
| 128 |
+
<p class="mt-1 text-3xl font-bold text-rose-700">@atRiskCount</p>
|
| 129 |
+
</div>
|
| 130 |
+
<span class="flex h-11 w-11 items-center justify-center rounded-xl bg-rose-100">
|
| 131 |
+
<i data-lucide="alert-triangle" class="h-6 w-6 text-rose-600"></i>
|
| 132 |
+
</span>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
@if (isChartView)
|
| 138 |
{
|
| 139 |
<!-- Chart View: SVG Project Progress Bars -->
|
|
|
|
| 241 |
private int currentPage = 1;
|
| 242 |
private int pageSize = 10;
|
| 243 |
|
| 244 |
+
// Card stats
|
| 245 |
+
private int totalActiveProjects = 0;
|
| 246 |
+
private int aheadOfScheduleCount = 0;
|
| 247 |
+
private int atRiskCount = 0;
|
| 248 |
+
|
| 249 |
private void ApplyAllFilters()
|
| 250 |
{
|
| 251 |
ApplyPeriodPreset();
|
|
|
|
| 352 |
filteredProjects = filteredProjects.Where(p => p.StartDate <= filterEndDate.Value.AddDays(1)).ToList();
|
| 353 |
}
|
| 354 |
|
| 355 |
+
var today = DateTime.Today;
|
| 356 |
+
|
| 357 |
foreach (var p in filteredProjects)
|
| 358 |
{
|
| 359 |
var projectTasks = tasks.Where(t => t.ProjectId == p.Id).ToList();
|
|
|
|
| 361 |
// Apply filter status on tasks if matching
|
| 362 |
if (filterStatus == "overdue")
|
| 363 |
{
|
| 364 |
+
projectTasks = projectTasks.Where(t => t.DueDate.Date < today && t.StatusId != 3).ToList();
|
| 365 |
}
|
| 366 |
else if (long.TryParse(filterStatus, out var statusId) && statusId > 0)
|
| 367 |
{
|
|
|
|
| 372 |
int completedTasks = projectTasks.Count(t => t.StatusId == 3); // Done = 3
|
| 373 |
double progress = totalTasks > 0 ? ((double)completedTasks / totalTasks) * 100 : 0;
|
| 374 |
|
| 375 |
+
// Ahead of schedule calculation
|
| 376 |
+
double elapsedPct;
|
| 377 |
+
if (today < p.StartDate) elapsedPct = 0;
|
| 378 |
+
else if (today > p.EndDate) elapsedPct = 100;
|
| 379 |
+
else
|
| 380 |
+
{
|
| 381 |
+
var totalDays = (p.EndDate - p.StartDate).TotalDays;
|
| 382 |
+
var elapsedDays = (today - p.StartDate).TotalDays;
|
| 383 |
+
elapsedPct = totalDays > 0 ? (elapsedDays / totalDays) * 100 : 100;
|
| 384 |
+
}
|
| 385 |
+
bool isAhead = progress > elapsedPct && progress > 0 && progress < 100;
|
| 386 |
+
|
| 387 |
+
// At risk calculation
|
| 388 |
+
bool isAtRisk = (projectTasks.Any(t => t.StatusId != 3 && t.DueDate.Date < today)) || (p.EndDate.Date < today && progress < 100);
|
| 389 |
+
|
| 390 |
metrics.Add(new ProjectMetric
|
| 391 |
{
|
| 392 |
ProjectName = p.Name,
|
|
|
|
| 394 |
EndDate = p.EndDate,
|
| 395 |
TotalTasks = totalTasks,
|
| 396 |
CompletedTasks = completedTasks,
|
| 397 |
+
Progress = progress,
|
| 398 |
+
IsAhead = isAhead,
|
| 399 |
+
IsAtRisk = isAtRisk
|
| 400 |
});
|
| 401 |
}
|
| 402 |
|
| 403 |
displayList = metrics.OrderByDescending(m => m.Progress).ToList();
|
| 404 |
+
|
| 405 |
+
// Calculate card statistics
|
| 406 |
+
totalActiveProjects = displayList.Count;
|
| 407 |
+
aheadOfScheduleCount = displayList.Count(m => m.IsAhead);
|
| 408 |
+
atRiskCount = displayList.Count(m => m.IsAtRisk);
|
| 409 |
}
|
| 410 |
|
| 411 |
private async Task DownloadCsv()
|
|
|
|
| 429 |
public int TotalTasks { get; set; }
|
| 430 |
public int CompletedTasks { get; set; }
|
| 431 |
public double Progress { get; set; }
|
| 432 |
+
public bool IsAhead { get; set; }
|
| 433 |
+
public bool IsAtRisk { get; set; }
|
| 434 |
}
|
| 435 |
}
|
TaskTrackingSystem.WebApp/Components/Pages/Projects.razor
CHANGED
|
@@ -177,6 +177,10 @@
|
|
| 177 |
</div>
|
| 178 |
</div>
|
| 179 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
@if (isDateError)
|
| 181 |
{
|
| 182 |
<p class="mt-1 text-xs text-red-500">@dateErrorMessage</p>
|
|
@@ -358,6 +362,7 @@
|
|
| 358 |
private string? formDescription;
|
| 359 |
private DateTime formStartDate = DateTime.Today;
|
| 360 |
private DateTime formEndDate = DateTime.Today.AddMonths(1);
|
|
|
|
| 361 |
|
| 362 |
// Validation states
|
| 363 |
private bool isNameError;
|
|
@@ -439,6 +444,7 @@
|
|
| 439 |
formDescription = null;
|
| 440 |
formStartDate = DateTime.Today;
|
| 441 |
formEndDate = DateTime.Today.AddMonths(1);
|
|
|
|
| 442 |
ClearValidation();
|
| 443 |
showModal = true;
|
| 444 |
}
|
|
@@ -451,6 +457,7 @@
|
|
| 451 |
formDescription = project.Description;
|
| 452 |
formStartDate = project.StartDate;
|
| 453 |
formEndDate = project.EndDate;
|
|
|
|
| 454 |
ClearValidation();
|
| 455 |
showModal = true;
|
| 456 |
}
|
|
@@ -639,7 +646,8 @@
|
|
| 639 |
Name = formName,
|
| 640 |
Description = formDescription,
|
| 641 |
StartDate = formStartDate,
|
| 642 |
-
EndDate = formEndDate
|
|
|
|
| 643 |
};
|
| 644 |
var response = await client.PutAsJsonAsync($"Project/{editingId}", dto);
|
| 645 |
var res = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
|
|
@@ -657,7 +665,8 @@
|
|
| 657 |
Description = formDescription,
|
| 658 |
StartDate = formStartDate,
|
| 659 |
EndDate = formEndDate,
|
| 660 |
-
CreatedById = currentUserId
|
|
|
|
| 661 |
};
|
| 662 |
var response = await client.PostAsJsonAsync("Project", dto);
|
| 663 |
var res = await response.Content.ReadFromJsonAsync<Result<ProjectDto>>(Serialization.CaseInsensitive);
|
|
|
|
| 177 |
</div>
|
| 178 |
</div>
|
| 179 |
</div>
|
| 180 |
+
<div>
|
| 181 |
+
<label class="block text-sm font-medium text-slate-700 mb-1.5 font-sans">Budgeted Hours</label>
|
| 182 |
+
<input @bind="formBudgetedHours" type="number" min="0" placeholder="Budgeted hours (optional)" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all font-sans" />
|
| 183 |
+
</div>
|
| 184 |
@if (isDateError)
|
| 185 |
{
|
| 186 |
<p class="mt-1 text-xs text-red-500">@dateErrorMessage</p>
|
|
|
|
| 362 |
private string? formDescription;
|
| 363 |
private DateTime formStartDate = DateTime.Today;
|
| 364 |
private DateTime formEndDate = DateTime.Today.AddMonths(1);
|
| 365 |
+
private int? formBudgetedHours;
|
| 366 |
|
| 367 |
// Validation states
|
| 368 |
private bool isNameError;
|
|
|
|
| 444 |
formDescription = null;
|
| 445 |
formStartDate = DateTime.Today;
|
| 446 |
formEndDate = DateTime.Today.AddMonths(1);
|
| 447 |
+
formBudgetedHours = null;
|
| 448 |
ClearValidation();
|
| 449 |
showModal = true;
|
| 450 |
}
|
|
|
|
| 457 |
formDescription = project.Description;
|
| 458 |
formStartDate = project.StartDate;
|
| 459 |
formEndDate = project.EndDate;
|
| 460 |
+
formBudgetedHours = project.BudgetedHours;
|
| 461 |
ClearValidation();
|
| 462 |
showModal = true;
|
| 463 |
}
|
|
|
|
| 646 |
Name = formName,
|
| 647 |
Description = formDescription,
|
| 648 |
StartDate = formStartDate,
|
| 649 |
+
EndDate = formEndDate,
|
| 650 |
+
BudgetedHours = formBudgetedHours
|
| 651 |
};
|
| 652 |
var response = await client.PutAsJsonAsync($"Project/{editingId}", dto);
|
| 653 |
var res = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
|
|
|
|
| 665 |
Description = formDescription,
|
| 666 |
StartDate = formStartDate,
|
| 667 |
EndDate = formEndDate,
|
| 668 |
+
CreatedById = currentUserId,
|
| 669 |
+
BudgetedHours = formBudgetedHours
|
| 670 |
};
|
| 671 |
var response = await client.PostAsJsonAsync("Project", dto);
|
| 672 |
var res = await response.Content.ReadFromJsonAsync<Result<ProjectDto>>(Serialization.CaseInsensitive);
|
TaskTrackingSystem.WebApp/Components/Pages/TaskAssign.razor
CHANGED
|
@@ -253,6 +253,7 @@
|
|
| 253 |
private List<ProjectDto> projects = new();
|
| 254 |
private List<UserDto> allUsers = new();
|
| 255 |
private List<RoleDto> roles = new();
|
|
|
|
| 256 |
|
| 257 |
private long selectedProjectId = 0;
|
| 258 |
private long selectedProjectIdInput = 0;
|
|
@@ -279,7 +280,11 @@
|
|
| 279 |
tasks = tasksTask.Result ?? new();
|
| 280 |
projects = projectsTask.Result ?? new();
|
| 281 |
allUsers = usersTask.Result ?? new();
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
}
|
| 284 |
catch (Exception ex)
|
| 285 |
{
|
|
@@ -319,6 +324,10 @@
|
|
| 319 |
get
|
| 320 |
{
|
| 321 |
var result = allUsers.Where(u => u.IsActive);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
if (selectedRoleId > 0)
|
| 323 |
{
|
| 324 |
result = result.Where(u => u.RoleId == selectedRoleId);
|
|
|
|
| 253 |
private List<ProjectDto> projects = new();
|
| 254 |
private List<UserDto> allUsers = new();
|
| 255 |
private List<RoleDto> roles = new();
|
| 256 |
+
private long? adminRoleId;
|
| 257 |
|
| 258 |
private long selectedProjectId = 0;
|
| 259 |
private long selectedProjectIdInput = 0;
|
|
|
|
| 280 |
tasks = tasksTask.Result ?? new();
|
| 281 |
projects = projectsTask.Result ?? new();
|
| 282 |
allUsers = usersTask.Result ?? new();
|
| 283 |
+
|
| 284 |
+
var allLoadedRoles = rolesTask.Result ?? new();
|
| 285 |
+
var adminRole = allLoadedRoles.FirstOrDefault(r => string.Equals(r.Name, "Admin", StringComparison.OrdinalIgnoreCase));
|
| 286 |
+
adminRoleId = adminRole?.Id;
|
| 287 |
+
roles = allLoadedRoles.Where(r => !string.Equals(r.Name, "Admin", StringComparison.OrdinalIgnoreCase)).ToList();
|
| 288 |
}
|
| 289 |
catch (Exception ex)
|
| 290 |
{
|
|
|
|
| 324 |
get
|
| 325 |
{
|
| 326 |
var result = allUsers.Where(u => u.IsActive);
|
| 327 |
+
if (adminRoleId.HasValue)
|
| 328 |
+
{
|
| 329 |
+
result = result.Where(u => u.RoleId != adminRoleId.Value);
|
| 330 |
+
}
|
| 331 |
if (selectedRoleId > 0)
|
| 332 |
{
|
| 333 |
result = result.Where(u => u.RoleId == selectedRoleId);
|
TaskTrackingSystem.WebApp/Components/Pages/TaskReport.razor
CHANGED
|
@@ -67,6 +67,7 @@
|
|
| 67 |
<div>
|
| 68 |
<label class="block text-xs font-medium text-slate-500 mb-1.5">Status</label>
|
| 69 |
<select @bind="filterStatus" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
|
|
|
|
| 70 |
<option value="">All</option>
|
| 71 |
<option value="1">To Do</option>
|
| 72 |
<option value="2">In Progress</option>
|
|
@@ -273,9 +274,19 @@
|
|
| 273 |
else
|
| 274 |
{
|
| 275 |
<tr>
|
| 276 |
-
<td colspan="6" class="px-6 py-
|
| 277 |
-
<
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
</td>
|
| 280 |
</tr>
|
| 281 |
}
|
|
@@ -304,7 +315,7 @@
|
|
| 304 |
private string filterPeriod = "all";
|
| 305 |
private DateTime? filterStartDate;
|
| 306 |
private DateTime? filterEndDate;
|
| 307 |
-
private string? filterStatus;
|
| 308 |
private int filterProjectId;
|
| 309 |
private string projectSearchInput = string.Empty;
|
| 310 |
private string projectSearchQuery = string.Empty;
|
|
@@ -337,7 +348,7 @@
|
|
| 337 |
filterPeriod = "all";
|
| 338 |
filterStartDate = null;
|
| 339 |
filterEndDate = null;
|
| 340 |
-
filterStatus =
|
| 341 |
filterProjectId = 0;
|
| 342 |
projectSearchInput = string.Empty;
|
| 343 |
projectSearchQuery = string.Empty;
|
|
|
|
| 67 |
<div>
|
| 68 |
<label class="block text-xs font-medium text-slate-500 mb-1.5">Status</label>
|
| 69 |
<select @bind="filterStatus" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
|
| 70 |
+
<option value="uncompleted">Uncompleted</option>
|
| 71 |
<option value="">All</option>
|
| 72 |
<option value="1">To Do</option>
|
| 73 |
<option value="2">In Progress</option>
|
|
|
|
| 274 |
else
|
| 275 |
{
|
| 276 |
<tr>
|
| 277 |
+
<td colspan="6" class="px-6 py-16 text-center bg-slate-50/20">
|
| 278 |
+
<div class="flex flex-col items-center justify-center space-y-4">
|
| 279 |
+
<svg class="w-24 h-24 text-slate-300" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 280 |
+
<rect x="25" y="15" width="70" height="90" rx="8" stroke="currentColor" stroke-width="3" stroke-dasharray="6 4" />
|
| 281 |
+
<path d="M45 40H75" stroke="currentColor" stroke-width="3" stroke-linecap="round" />
|
| 282 |
+
<path d="M45 55H75" stroke="currentColor" stroke-width="3" stroke-linecap="round" />
|
| 283 |
+
<path d="M45 70H60" stroke="currentColor" stroke-width="3" stroke-linecap="round" />
|
| 284 |
+
<circle cx="85" cy="85" r="15" fill="white" stroke="#7c3aed" stroke-width="3" />
|
| 285 |
+
<path d="M80 85L90 85M85 80L85 90" stroke="#7c3aed" stroke-width="3" stroke-linecap="round" />
|
| 286 |
+
</svg>
|
| 287 |
+
<h3 class="text-lg font-semibold text-slate-700">No Tasks Found</h3>
|
| 288 |
+
<p class="text-sm text-slate-400 max-w-xs">There are no tasks matching your filters. Try adjusting your search query or filters.</p>
|
| 289 |
+
</div>
|
| 290 |
</td>
|
| 291 |
</tr>
|
| 292 |
}
|
|
|
|
| 315 |
private string filterPeriod = "all";
|
| 316 |
private DateTime? filterStartDate;
|
| 317 |
private DateTime? filterEndDate;
|
| 318 |
+
private string? filterStatus = "uncompleted";
|
| 319 |
private int filterProjectId;
|
| 320 |
private string projectSearchInput = string.Empty;
|
| 321 |
private string projectSearchQuery = string.Empty;
|
|
|
|
| 348 |
filterPeriod = "all";
|
| 349 |
filterStartDate = null;
|
| 350 |
filterEndDate = null;
|
| 351 |
+
filterStatus = "uncompleted";
|
| 352 |
filterProjectId = 0;
|
| 353 |
projectSearchInput = string.Empty;
|
| 354 |
projectSearchQuery = string.Empty;
|
TaskTrackingSystem.WebApp/Components/Pages/Tasks.razor
CHANGED
|
@@ -634,7 +634,7 @@
|
|
| 634 |
private long formPriorityId = 2;
|
| 635 |
private DateTime formDueDate = DateTime.Today.AddDays(7);
|
| 636 |
private long? formAssignedTo;
|
| 637 |
-
private
|
| 638 |
|
| 639 |
protected override async Task OnInitializedAsync()
|
| 640 |
{
|
|
|
|
| 634 |
private long formPriorityId = 2;
|
| 635 |
private DateTime formDueDate = DateTime.Today.AddDays(7);
|
| 636 |
private long? formAssignedTo;
|
| 637 |
+
private decimal? formEstimatedHours;
|
| 638 |
|
| 639 |
protected override async Task OnInitializedAsync()
|
| 640 |
{
|
TaskTrackingSystem.WebApp/Components/Pages/TimeTrackingReport.razor
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
@using TaskTrackingSystem.Shared.Models.Task
|
| 4 |
@using TaskTrackingSystem.Shared.Models.User
|
| 5 |
@using TaskTrackingSystem.Shared.Models.Project
|
|
|
|
| 6 |
@using System.Text
|
| 7 |
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
| 8 |
@attribute [Authorize]
|
|
@@ -16,25 +17,25 @@
|
|
| 16 |
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
| 17 |
<div>
|
| 18 |
<h2 class="text-3xl font-bold tracking-tight text-slate-900">Time Tracking & Timesheet</h2>
|
| 19 |
-
<p class="text-slate-500 mt-1">Estimated vs actual hours breakdown by user and project.</p>
|
| 20 |
</div>
|
| 21 |
<div class="flex items-center space-x-3">
|
| 22 |
<div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
|
| 23 |
<button @onclick='() => currentView = "user"'
|
| 24 |
-
class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(currentView == "user" ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
|
| 25 |
-
<i data-lucide="users" class="h-3.5 w-3.5 mr-1"></i> By User
|
| 26 |
</button>
|
| 27 |
<button @onclick='() => currentView = "project"'
|
| 28 |
-
class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(currentView == "project" ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
|
| 29 |
-
<i data-lucide="folder-kanban" class="h-3.5 w-3.5 mr-1"></i> By Project
|
| 30 |
</button>
|
| 31 |
<button @onclick='() => currentView = "detail"'
|
| 32 |
-
class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(currentView == "detail" ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
|
| 33 |
-
<i data-lucide="list" class="h-3.5 w-3.5 mr-1"></i> Detail
|
| 34 |
</button>
|
| 35 |
</div>
|
| 36 |
-
<button @onclick="DownloadCsv" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm">
|
| 37 |
-
<i data-lucide="download" class="h-4 w-4 mr-2"></i> Export CSV
|
| 38 |
</button>
|
| 39 |
</div>
|
| 40 |
</div>
|
|
@@ -46,8 +47,8 @@
|
|
| 46 |
<div class="rounded-xl border border-violet-100 bg-violet-50 p-5 shadow-sm">
|
| 47 |
<div class="flex items-center justify-between">
|
| 48 |
<div>
|
| 49 |
-
<p class="text-xs font-semibold uppercase tracking-wider text-violet-500">Total Estimated Hrs</p>
|
| 50 |
-
<p class="mt-1 text-3xl font-bold text-violet-700">@
|
| 51 |
</div>
|
| 52 |
<span class="flex h-11 w-11 items-center justify-center rounded-xl bg-violet-100">
|
| 53 |
<i data-lucide="clock" class="h-6 w-6 text-violet-600"></i>
|
|
@@ -57,8 +58,8 @@
|
|
| 57 |
<div class="rounded-xl border border-emerald-100 bg-emerald-50 p-5 shadow-sm">
|
| 58 |
<div class="flex items-center justify-between">
|
| 59 |
<div>
|
| 60 |
-
<p class="text-xs font-semibold uppercase tracking-wider text-emerald-500">Tasks with Hours</p>
|
| 61 |
-
<p class="mt-1 text-3xl font-bold text-emerald-700">@
|
| 62 |
</div>
|
| 63 |
<span class="flex h-11 w-11 items-center justify-center rounded-xl bg-emerald-100">
|
| 64 |
<i data-lucide="check-circle" class="h-6 w-6 text-emerald-600"></i>
|
|
@@ -68,8 +69,8 @@
|
|
| 68 |
<div class="rounded-xl border border-blue-100 bg-blue-50 p-5 shadow-sm">
|
| 69 |
<div class="flex items-center justify-between">
|
| 70 |
<div>
|
| 71 |
-
<p class="text-xs font-semibold uppercase tracking-wider text-blue-500">Avg Hrs / Task</p>
|
| 72 |
-
<p class="mt-1 text-3xl font-bold text-blue-700">@
|
| 73 |
</div>
|
| 74 |
<span class="flex h-11 w-11 items-center justify-center rounded-xl bg-blue-100">
|
| 75 |
<i data-lucide="bar-chart-2" class="h-6 w-6 text-blue-600"></i>
|
|
@@ -79,8 +80,8 @@
|
|
| 79 |
<div class="rounded-xl border border-amber-100 bg-amber-50 p-5 shadow-sm">
|
| 80 |
<div class="flex items-center justify-between">
|
| 81 |
<div>
|
| 82 |
-
<p class="text-xs font-semibold uppercase tracking-wider text-amber-500">Completed Hours</p>
|
| 83 |
-
<p class="mt-1 text-3xl font-bold text-amber-700">@
|
| 84 |
</div>
|
| 85 |
<span class="flex h-11 w-11 items-center justify-center rounded-xl bg-amber-100">
|
| 86 |
<i data-lucide="trophy" class="h-6 w-6 text-amber-600"></i>
|
|
@@ -94,34 +95,34 @@
|
|
| 94 |
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
|
| 95 |
<div class="flex flex-wrap items-end gap-4">
|
| 96 |
<div class="flex-1 min-w-[200px]">
|
| 97 |
-
<label class="block text-xs font-medium text-slate-500 mb-1.5">Search</label>
|
| 98 |
<input @bind="searchInput" type="text" placeholder="Search by user, task, or project..."
|
| 99 |
-
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 transition-all" />
|
| 100 |
</div>
|
| 101 |
<div>
|
| 102 |
-
<label class="block text-xs font-medium text-slate-500 mb-1.5">Project</label>
|
| 103 |
-
<select @bind="filterProjectId" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
|
| 104 |
<option value="0">All Projects</option>
|
| 105 |
@foreach (var p in allProjects)
|
| 106 |
{
|
| 107 |
-
<option value="@p.Id">@p.Name</option>
|
| 108 |
}
|
| 109 |
</select>
|
| 110 |
</div>
|
| 111 |
<div>
|
| 112 |
-
<label class="block text-xs font-medium text-slate-500 mb-1.5">Status</label>
|
| 113 |
-
<select @bind="filterStatusId" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
|
| 114 |
-
<option value="0">All</option>
|
| 115 |
-
<option value="1">To Do</option>
|
| 116 |
-
<option value="2">In Progress</option>
|
| 117 |
-
<option value="3">Done</option>
|
| 118 |
</select>
|
| 119 |
</div>
|
| 120 |
-
<button @onclick="ApplyFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
|
| 121 |
-
<i data-lucide="search" class="h-4 w-4 mr-2"></i> Search
|
| 122 |
</button>
|
| 123 |
-
<button @onclick="ResetFilters" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-50 transition-colors shadow-sm">
|
| 124 |
-
<i data-lucide="rotate-ccw" class="h-4 w-4 mr-2"></i> Reset
|
| 125 |
</button>
|
| 126 |
</div>
|
| 127 |
</div>
|
|
@@ -137,66 +138,52 @@
|
|
| 137 |
{
|
| 138 |
<div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
| 139 |
<div class="px-6 py-4 border-b border-slate-100">
|
| 140 |
-
<h3 class="text-sm font-semibold text-slate-700">Hours Summary by Employee</h3>
|
| 141 |
</div>
|
| 142 |
<table class="w-full">
|
| 143 |
<thead>
|
| 144 |
-
<tr class="border-b border-slate-100 bg-slate-50/60">
|
| 145 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Employee</th>
|
| 146 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Total Tasks</th>
|
| 147 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Completed</th>
|
| 148 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Est. Hours</th>
|
| 149 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Completed Hrs</th>
|
| 150 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
|
|
|
| 151 |
</tr>
|
| 152 |
</thead>
|
| 153 |
<tbody class="divide-y divide-slate-100">
|
| 154 |
-
@
|
| 155 |
-
var userGroups = GetFilteredTasks()
|
| 156 |
-
.GroupBy(t => t.AssignedTo)
|
| 157 |
-
.OrderByDescending(g => g.Sum(t => t.EstimatedHours ?? 0))
|
| 158 |
-
.ToList();
|
| 159 |
-
}
|
| 160 |
-
@if (userGroups.Any())
|
| 161 |
{
|
| 162 |
-
@foreach (var
|
| 163 |
{
|
| 164 |
-
var
|
| 165 |
-
var totalTasks = group.Count();
|
| 166 |
-
var completedTasks = group.Count(t => t.StatusId == 3);
|
| 167 |
-
var estHours = group.Sum(t => t.EstimatedHours ?? 0);
|
| 168 |
-
var compHours = group.Where(t => t.StatusId == 3).Sum(t => t.EstimatedHours ?? 0);
|
| 169 |
-
var pct = totalTasks > 0 ? (double)completedTasks / totalTasks * 100 : 0;
|
| 170 |
<tr class="hover:bg-slate-50/50 transition-colors">
|
| 171 |
<td class="px-6 py-4">
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
</
|
| 178 |
-
<
|
| 179 |
-
<p class="text-sm font-semibold text-slate-900">@user.FirstName @user.LastName</p>
|
| 180 |
-
<p class="text-xs text-slate-400">@@@user.Username</p>
|
| 181 |
-
</div>
|
| 182 |
</div>
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
| 188 |
</td>
|
| 189 |
-
<td class="px-6 py-4 text-sm text-slate-700 font-medium">@totalTasks</td>
|
| 190 |
-
<td class="px-6 py-4 text-sm text-emerald-600 font-medium">@completedTasks</td>
|
| 191 |
-
<td class="px-6 py-4 text-sm font-semibold text-violet-700">@estHours h</td>
|
| 192 |
-
<td class="px-6 py-4 text-sm font-semibold text-amber-600">@compHours h</td>
|
| 193 |
<td class="px-6 py-4">
|
| 194 |
<div class="flex items-center gap-3">
|
| 195 |
<div class="flex-1 h-2 bg-slate-100 rounded-full overflow-hidden">
|
| 196 |
<div class="h-2 rounded-full @(pct >= 80 ? "bg-emerald-500" : pct >= 50 ? "bg-amber-400" : "bg-rose-400") transition-all"
|
| 197 |
style="width: @pct.ToString("F0")%"></div>
|
| 198 |
</div>
|
| 199 |
-
<span class="text-xs font-semibold text-slate-600 w-10">@pct.ToString("F0")%</span>
|
| 200 |
</div>
|
| 201 |
</td>
|
| 202 |
</tr>
|
|
@@ -204,7 +191,7 @@
|
|
| 204 |
}
|
| 205 |
else
|
| 206 |
{
|
| 207 |
-
<EmptyState ColSpan="
|
| 208 |
}
|
| 209 |
</tbody>
|
| 210 |
</table>
|
|
@@ -216,54 +203,43 @@
|
|
| 216 |
{
|
| 217 |
<div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
| 218 |
<div class="px-6 py-4 border-b border-slate-100">
|
| 219 |
-
<h3 class="text-sm font-semibold text-slate-700">Hours Summary by Project</h3>
|
| 220 |
</div>
|
| 221 |
<table class="w-full">
|
| 222 |
<thead>
|
| 223 |
<tr class="border-b border-slate-100 bg-slate-50/60">
|
| 224 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project</th>
|
| 225 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Total Tasks</th>
|
| 226 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Completed</th>
|
| 227 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Est. Hours</th>
|
| 228 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Completed Hrs</th>
|
| 229 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Completion %</th>
|
| 230 |
</tr>
|
| 231 |
</thead>
|
| 232 |
<tbody class="divide-y divide-slate-100">
|
| 233 |
-
@
|
| 234 |
-
var projGroups = GetFilteredTasks()
|
| 235 |
-
.GroupBy(t => t.ProjectId)
|
| 236 |
-
.OrderByDescending(g => g.Sum(t => t.EstimatedHours ?? 0))
|
| 237 |
-
.ToList();
|
| 238 |
-
}
|
| 239 |
-
@if (projGroups.Any())
|
| 240 |
{
|
| 241 |
-
@foreach (var
|
| 242 |
{
|
| 243 |
-
var
|
| 244 |
-
var totalTasks = group.Count();
|
| 245 |
-
var completedTasks = group.Count(t => t.StatusId == 3);
|
| 246 |
-
var estHours = group.Sum(t => t.EstimatedHours ?? 0);
|
| 247 |
-
var compHours = group.Where(t => t.StatusId == 3).Sum(t => t.EstimatedHours ?? 0);
|
| 248 |
-
var pct = totalTasks > 0 ? (double)completedTasks / totalTasks * 100 : 0;
|
| 249 |
<tr class="hover:bg-slate-50/50 transition-colors">
|
| 250 |
<td class="px-6 py-4">
|
| 251 |
<div class="flex items-center gap-2">
|
| 252 |
<i data-lucide="folder-kanban" class="h-4 w-4 text-violet-500"></i>
|
| 253 |
-
<span class="text-sm font-semibold text-slate-900">@
|
| 254 |
</div>
|
| 255 |
</td>
|
| 256 |
-
<td class="px-6 py-4 text-sm text-slate-700 font-medium">@
|
| 257 |
-
<td class="px-6 py-4 text-sm text-emerald-600 font-medium">@
|
| 258 |
-
<td class="px-6 py-4 text-sm font-semibold text-violet-700">@
|
| 259 |
-
<td class="px-6 py-4 text-sm font-semibold text-amber-600">@
|
| 260 |
<td class="px-6 py-4">
|
| 261 |
<div class="flex items-center gap-3">
|
| 262 |
<div class="flex-1 h-2 bg-slate-100 rounded-full overflow-hidden">
|
| 263 |
<div class="h-2 rounded-full @(pct >= 80 ? "bg-emerald-500" : pct >= 50 ? "bg-amber-400" : "bg-rose-400") transition-all"
|
| 264 |
style="width: @pct.ToString("F0")%"></div>
|
| 265 |
</div>
|
| 266 |
-
<span class="text-xs font-semibold text-slate-600 w-10">@pct.ToString("F0")%</span>
|
| 267 |
</div>
|
| 268 |
</td>
|
| 269 |
</tr>
|
|
@@ -283,19 +259,20 @@
|
|
| 283 |
{
|
| 284 |
<div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
| 285 |
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
|
| 286 |
-
<h3 class="text-sm font-semibold text-slate-700">
|
| 287 |
-
Task-level Detail — <span class="text-violet-600">@FilteredDetailTasks.Count() task(s)</span>
|
| 288 |
</h3>
|
| 289 |
</div>
|
| 290 |
<table class="w-full">
|
| 291 |
<thead>
|
| 292 |
<tr class="border-b border-slate-100 bg-slate-50/60">
|
| 293 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Task</th>
|
| 294 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project</th>
|
| 295 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assignee</th>
|
| 296 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Est. Hours</th>
|
| 297 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
| 298 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
|
|
|
| 299 |
</tr>
|
| 300 |
</thead>
|
| 301 |
<tbody class="divide-y divide-slate-100">
|
|
@@ -303,8 +280,6 @@
|
|
| 303 |
{
|
| 304 |
@foreach (var task in PagedDetailTasks)
|
| 305 |
{
|
| 306 |
-
var assignee = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo);
|
| 307 |
-
var project = allProjects.FirstOrDefault(p => p.Id == task.ProjectId);
|
| 308 |
var (statusLabel, statusClass) = task.StatusId switch {
|
| 309 |
2 => ("In Progress", "bg-blue-50 text-blue-700"),
|
| 310 |
3 => ("Done", "bg-emerald-50 text-emerald-700"),
|
|
@@ -314,39 +289,42 @@
|
|
| 314 |
<td class="px-6 py-4">
|
| 315 |
<p class="text-sm font-semibold text-slate-900">@task.Title</p>
|
| 316 |
</td>
|
| 317 |
-
<td class="px-6 py-4 text-sm text-slate-600">@
|
| 318 |
<td class="px-6 py-4">
|
| 319 |
-
@if (
|
| 320 |
{
|
| 321 |
<div class="flex items-center gap-2">
|
| 322 |
<span class="flex h-7 w-7 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold">
|
| 323 |
-
@(
|
| 324 |
</span>
|
| 325 |
-
<span class="text-sm text-slate-700">@
|
| 326 |
</div>
|
| 327 |
}
|
| 328 |
-
else { <span class="text-xs text-slate-400">Unassigned</span> }
|
| 329 |
</td>
|
| 330 |
<td class="px-6 py-4">
|
| 331 |
@if (task.EstimatedHours.HasValue)
|
| 332 |
{
|
| 333 |
-
<span class="text-sm font-semibold text-violet-700">@task.EstimatedHours h</span>
|
| 334 |
}
|
| 335 |
else
|
| 336 |
{
|
| 337 |
-
<span class="text-xs text-slate-400">—</span>
|
| 338 |
}
|
| 339 |
</td>
|
| 340 |
-
<td class="px-6 py-4 text-sm text-slate-600">@task.DueDate.ToString("dd-MMM-yyyy")</td>
|
| 341 |
<td class="px-6 py-4">
|
| 342 |
-
<span class="
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
</td>
|
| 344 |
</tr>
|
| 345 |
}
|
| 346 |
}
|
| 347 |
else
|
| 348 |
{
|
| 349 |
-
<EmptyState ColSpan="
|
| 350 |
}
|
| 351 |
</tbody>
|
| 352 |
</table>
|
|
@@ -367,7 +345,7 @@
|
|
| 367 |
private bool isLoading = true;
|
| 368 |
private string currentView = "user";
|
| 369 |
|
| 370 |
-
private
|
| 371 |
private List<UserDto> allUsers = new();
|
| 372 |
private List<ProjectDto> allProjects = new();
|
| 373 |
|
|
@@ -383,52 +361,23 @@
|
|
| 383 |
private int detailPage = 1;
|
| 384 |
private int pageSize = 10;
|
| 385 |
|
| 386 |
-
|
| 387 |
-
private int totalEstimated = 0;
|
| 388 |
-
private int tasksWithHours = 0;
|
| 389 |
-
private int avgHoursPerTask = 0;
|
| 390 |
-
private int completedHours = 0;
|
| 391 |
-
|
| 392 |
-
private IEnumerable<TaskDto> GetFilteredTasks()
|
| 393 |
-
{
|
| 394 |
-
var result = allTasks.AsEnumerable();
|
| 395 |
-
if (!string.IsNullOrWhiteSpace(appliedSearch))
|
| 396 |
-
{
|
| 397 |
-
var q = appliedSearch.Trim().ToLower();
|
| 398 |
-
result = result.Where(t =>
|
| 399 |
-
t.Title.ToLower().Contains(q) ||
|
| 400 |
-
(allUsers.FirstOrDefault(u => u.Id == t.AssignedTo) is { } usr &&
|
| 401 |
-
($"{usr.FirstName} {usr.LastName}").ToLower().Contains(q)) ||
|
| 402 |
-
(allProjects.FirstOrDefault(p => p.Id == t.ProjectId) is { } prj &&
|
| 403 |
-
prj.Name.ToLower().Contains(q))
|
| 404 |
-
);
|
| 405 |
-
}
|
| 406 |
-
if (appliedProjectId > 0)
|
| 407 |
-
result = result.Where(t => t.ProjectId == appliedProjectId);
|
| 408 |
-
if (appliedStatusId > 0)
|
| 409 |
-
result = result.Where(t => t.StatusId == appliedStatusId);
|
| 410 |
-
return result;
|
| 411 |
-
}
|
| 412 |
-
|
| 413 |
-
private IEnumerable<TaskDto> FilteredDetailTasks => GetFilteredTasks().OrderBy(t => t.ProjectId).ThenBy(t => t.Title);
|
| 414 |
private int DetailTotalPages => (int)Math.Ceiling((double)FilteredDetailTasks.Count() / pageSize);
|
| 415 |
-
private IEnumerable<
|
| 416 |
|
| 417 |
protected override async Task OnInitializedAsync()
|
| 418 |
{
|
| 419 |
var client = ApiClient.CreateClient();
|
| 420 |
try
|
| 421 |
{
|
| 422 |
-
var tasksTask = client.GetFromJsonAsync<List<TaskDto>>("Task", Serialization.CaseInsensitive);
|
| 423 |
var usersTask = client.GetFromJsonAsync<List<UserDto>>("User", Serialization.CaseInsensitive);
|
| 424 |
var projectsTask = client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive);
|
| 425 |
-
await System.Threading.Tasks.Task.WhenAll(
|
| 426 |
|
| 427 |
-
allTasks = tasksTask.Result ?? new();
|
| 428 |
allUsers = usersTask.Result ?? new();
|
| 429 |
allProjects = projectsTask.Result ?? new();
|
| 430 |
|
| 431 |
-
|
| 432 |
}
|
| 433 |
catch (Exception ex) { Console.WriteLine($"Load error: {ex.Message}"); }
|
| 434 |
finally { isLoading = false; }
|
|
@@ -439,29 +388,47 @@
|
|
| 439 |
await JS.InvokeVoidAsync("initIcons");
|
| 440 |
}
|
| 441 |
|
| 442 |
-
private
|
| 443 |
{
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
}
|
| 450 |
|
| 451 |
-
private
|
| 452 |
{
|
| 453 |
appliedSearch = searchInput;
|
| 454 |
appliedProjectId = filterProjectId;
|
| 455 |
appliedStatusId = filterStatusId;
|
| 456 |
detailPage = 1;
|
|
|
|
| 457 |
}
|
| 458 |
|
| 459 |
-
private
|
| 460 |
{
|
| 461 |
searchInput = ""; appliedSearch = "";
|
| 462 |
filterProjectId = 0; appliedProjectId = 0;
|
| 463 |
filterStatusId = 0; appliedStatusId = 0;
|
| 464 |
detailPage = 1;
|
|
|
|
| 465 |
}
|
| 466 |
|
| 467 |
private void HandleDetailPageChanged(int page) => detailPage = page;
|
|
@@ -470,13 +437,11 @@
|
|
| 470 |
private async Task DownloadCsv()
|
| 471 |
{
|
| 472 |
var sb = new StringBuilder();
|
| 473 |
-
sb.AppendLine("Task Title,Project,Assignee,Estimated Hours,Due Date,Status");
|
| 474 |
-
foreach (var task in
|
| 475 |
{
|
| 476 |
-
var
|
| 477 |
-
|
| 478 |
-
var status = task.StatusId switch { 2 => "In Progress", 3 => "Done", _ => "To Do" };
|
| 479 |
-
sb.AppendLine($"\"{task.Title}\",\"{project?.Name ?? ""}\",\"{(assignee != null ? $"{assignee.FirstName} {assignee.LastName}" : "Unassigned")}\",{task.EstimatedHours?.ToString() ?? ""},\"{task.DueDate:dd-MMM-yyyy}\",{status}");
|
| 480 |
}
|
| 481 |
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
| 482 |
var base64 = Convert.ToBase64String(bytes);
|
|
|
|
| 3 |
@using TaskTrackingSystem.Shared.Models.Task
|
| 4 |
@using TaskTrackingSystem.Shared.Models.User
|
| 5 |
@using TaskTrackingSystem.Shared.Models.Project
|
| 6 |
+
@using TaskTrackingSystem.Shared.Models.Report
|
| 7 |
@using System.Text
|
| 8 |
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
| 9 |
@attribute [Authorize]
|
|
|
|
| 17 |
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
| 18 |
<div>
|
| 19 |
<h2 class="text-3xl font-bold tracking-tight text-slate-900">Time Tracking & Timesheet</h2>
|
| 20 |
+
<p class="text-slate-500 mt-1 font-sans">Estimated vs actual hours breakdown by user and project.</p>
|
| 21 |
</div>
|
| 22 |
<div class="flex items-center space-x-3">
|
| 23 |
<div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
|
| 24 |
<button @onclick='() => currentView = "user"'
|
| 25 |
+
class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(currentView == "user" ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors font-sans">
|
| 26 |
+
<i data-lucide="users" class="h-3.5 w-3.5 mr-1 font-sans"></i> By User
|
| 27 |
</button>
|
| 28 |
<button @onclick='() => currentView = "project"'
|
| 29 |
+
class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(currentView == "project" ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors font-sans">
|
| 30 |
+
<i data-lucide="folder-kanban" class="h-3.5 w-3.5 mr-1 font-sans"></i> By Project
|
| 31 |
</button>
|
| 32 |
<button @onclick='() => currentView = "detail"'
|
| 33 |
+
class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(currentView == "detail" ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors font-sans">
|
| 34 |
+
<i data-lucide="list" class="h-3.5 w-3.5 mr-1 font-sans"></i> Detail
|
| 35 |
</button>
|
| 36 |
</div>
|
| 37 |
+
<button @onclick="DownloadCsv" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm font-sans">
|
| 38 |
+
<i data-lucide="download" class="h-4 w-4 mr-2 font-sans"></i> Export CSV
|
| 39 |
</button>
|
| 40 |
</div>
|
| 41 |
</div>
|
|
|
|
| 47 |
<div class="rounded-xl border border-violet-100 bg-violet-50 p-5 shadow-sm">
|
| 48 |
<div class="flex items-center justify-between">
|
| 49 |
<div>
|
| 50 |
+
<p class="text-xs font-semibold uppercase tracking-wider text-violet-500 font-sans">Total Estimated Hrs</p>
|
| 51 |
+
<p class="mt-1 text-3xl font-bold text-violet-700">@reportData.TotalEstimatedHrs.ToString("F1")</p>
|
| 52 |
</div>
|
| 53 |
<span class="flex h-11 w-11 items-center justify-center rounded-xl bg-violet-100">
|
| 54 |
<i data-lucide="clock" class="h-6 w-6 text-violet-600"></i>
|
|
|
|
| 58 |
<div class="rounded-xl border border-emerald-100 bg-emerald-50 p-5 shadow-sm">
|
| 59 |
<div class="flex items-center justify-between">
|
| 60 |
<div>
|
| 61 |
+
<p class="text-xs font-semibold uppercase tracking-wider text-emerald-500 font-sans">Tasks with Hours</p>
|
| 62 |
+
<p class="mt-1 text-3xl font-bold text-emerald-700">@reportData.TasksWithHours</p>
|
| 63 |
</div>
|
| 64 |
<span class="flex h-11 w-11 items-center justify-center rounded-xl bg-emerald-100">
|
| 65 |
<i data-lucide="check-circle" class="h-6 w-6 text-emerald-600"></i>
|
|
|
|
| 69 |
<div class="rounded-xl border border-blue-100 bg-blue-50 p-5 shadow-sm">
|
| 70 |
<div class="flex items-center justify-between">
|
| 71 |
<div>
|
| 72 |
+
<p class="text-xs font-semibold uppercase tracking-wider text-blue-500 font-sans">Avg Hrs / Task</p>
|
| 73 |
+
<p class="mt-1 text-3xl font-bold text-blue-700">@reportData.AvgHoursPerTask.ToString("F1")</p>
|
| 74 |
</div>
|
| 75 |
<span class="flex h-11 w-11 items-center justify-center rounded-xl bg-blue-100">
|
| 76 |
<i data-lucide="bar-chart-2" class="h-6 w-6 text-blue-600"></i>
|
|
|
|
| 80 |
<div class="rounded-xl border border-amber-100 bg-amber-50 p-5 shadow-sm">
|
| 81 |
<div class="flex items-center justify-between">
|
| 82 |
<div>
|
| 83 |
+
<p class="text-xs font-semibold uppercase tracking-wider text-amber-500 font-sans">Completed Hours</p>
|
| 84 |
+
<p class="mt-1 text-3xl font-bold text-amber-700">@reportData.CompletedHours.ToString("F1")</p>
|
| 85 |
</div>
|
| 86 |
<span class="flex h-11 w-11 items-center justify-center rounded-xl bg-amber-100">
|
| 87 |
<i data-lucide="trophy" class="h-6 w-6 text-amber-600"></i>
|
|
|
|
| 95 |
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
|
| 96 |
<div class="flex flex-wrap items-end gap-4">
|
| 97 |
<div class="flex-1 min-w-[200px]">
|
| 98 |
+
<label class="block text-xs font-medium text-slate-500 mb-1.5 font-sans">Search</label>
|
| 99 |
<input @bind="searchInput" type="text" placeholder="Search by user, task, or project..."
|
| 100 |
+
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 transition-all font-sans" />
|
| 101 |
</div>
|
| 102 |
<div>
|
| 103 |
+
<label class="block text-xs font-medium text-slate-500 mb-1.5 font-sans">Project</label>
|
| 104 |
+
<select @bind="filterProjectId" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all font-sans">
|
| 105 |
<option value="0">All Projects</option>
|
| 106 |
@foreach (var p in allProjects)
|
| 107 |
{
|
| 108 |
+
<option value="@p.Id font-sans">@p.Name</option>
|
| 109 |
}
|
| 110 |
</select>
|
| 111 |
</div>
|
| 112 |
<div>
|
| 113 |
+
<label class="block text-xs font-medium text-slate-500 mb-1.5 font-sans">Status</label>
|
| 114 |
+
<select @bind="filterStatusId" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all font-sans">
|
| 115 |
+
<option value="0 font-sans">All</option>
|
| 116 |
+
<option value="1 font-sans">To Do</option>
|
| 117 |
+
<option value="2 font-sans">In Progress</option>
|
| 118 |
+
<option value="3 font-sans">Done</option>
|
| 119 |
</select>
|
| 120 |
</div>
|
| 121 |
+
<button @onclick="ApplyFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm font-sans">
|
| 122 |
+
<i data-lucide="search" class="h-4 w-4 mr-2 font-sans"></i> Search
|
| 123 |
</button>
|
| 124 |
+
<button @onclick="ResetFilters" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-50 transition-colors shadow-sm font-sans">
|
| 125 |
+
<i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 font-sans"></i> Reset
|
| 126 |
</button>
|
| 127 |
</div>
|
| 128 |
</div>
|
|
|
|
| 138 |
{
|
| 139 |
<div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
| 140 |
<div class="px-6 py-4 border-b border-slate-100">
|
| 141 |
+
<h3 class="text-sm font-semibold text-slate-700 font-sans">Hours Summary by Employee</h3>
|
| 142 |
</div>
|
| 143 |
<table class="w-full">
|
| 144 |
<thead>
|
| 145 |
+
<tr class="border-b border-slate-100 bg-slate-50/60 font-sans">
|
| 146 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Employee</th>
|
| 147 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Total Tasks</th>
|
| 148 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Completed</th>
|
| 149 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Est. Hours</th>
|
| 150 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Completed Hrs</th>
|
| 151 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Variance</th>
|
| 152 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Completion %</th>
|
| 153 |
</tr>
|
| 154 |
</thead>
|
| 155 |
<tbody class="divide-y divide-slate-100">
|
| 156 |
+
@if (reportData.EmployeeSummary.Any())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
{
|
| 158 |
+
@foreach (var emp in reportData.EmployeeSummary)
|
| 159 |
{
|
| 160 |
+
var pct = emp.CompletionPercentage;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
<tr class="hover:bg-slate-50/50 transition-colors">
|
| 162 |
<td class="px-6 py-4">
|
| 163 |
+
<div class="flex items-center gap-3">
|
| 164 |
+
<span class="flex h-9 w-9 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold">
|
| 165 |
+
@(emp.FullName.Length > 0 ? emp.FullName[0].ToString().ToUpper() : "")
|
| 166 |
+
</span>
|
| 167 |
+
<div>
|
| 168 |
+
<p class="text-sm font-semibold text-slate-900">@emp.FullName</p>
|
| 169 |
+
<p class="text-xs text-slate-400">@@@emp.Username</p>
|
|
|
|
|
|
|
|
|
|
| 170 |
</div>
|
| 171 |
+
</div>
|
| 172 |
+
</td>
|
| 173 |
+
<td class="px-6 py-4 text-sm text-slate-700 font-medium font-sans">@emp.TotalTasks</td>
|
| 174 |
+
<td class="px-6 py-4 text-sm text-emerald-600 font-medium font-sans">@emp.CompletedTasks</td>
|
| 175 |
+
<td class="px-6 py-4 text-sm font-semibold text-violet-700 font-sans">@emp.EstHours.ToString("F1") h</td>
|
| 176 |
+
<td class="px-6 py-4 text-sm font-semibold text-amber-600 font-sans">@emp.CompletedHours.ToString("F1") h</td>
|
| 177 |
+
<td class="px-6 py-4 text-sm font-semibold @(emp.CompletedHours > emp.EstHours ? "text-rose-600" : "text-emerald-600") font-sans">
|
| 178 |
+
@(emp.Variance.ToString("F1")) h
|
| 179 |
</td>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
<td class="px-6 py-4">
|
| 181 |
<div class="flex items-center gap-3">
|
| 182 |
<div class="flex-1 h-2 bg-slate-100 rounded-full overflow-hidden">
|
| 183 |
<div class="h-2 rounded-full @(pct >= 80 ? "bg-emerald-500" : pct >= 50 ? "bg-amber-400" : "bg-rose-400") transition-all"
|
| 184 |
style="width: @pct.ToString("F0")%"></div>
|
| 185 |
</div>
|
| 186 |
+
<span class="text-xs font-semibold text-slate-600 w-10 font-sans">@pct.ToString("F0")%</span>
|
| 187 |
</div>
|
| 188 |
</td>
|
| 189 |
</tr>
|
|
|
|
| 191 |
}
|
| 192 |
else
|
| 193 |
{
|
| 194 |
+
<EmptyState ColSpan="7" Icon="clock" Title="No timesheet data" Subtitle="No tasks found with the current filters." />
|
| 195 |
}
|
| 196 |
</tbody>
|
| 197 |
</table>
|
|
|
|
| 203 |
{
|
| 204 |
<div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
| 205 |
<div class="px-6 py-4 border-b border-slate-100">
|
| 206 |
+
<h3 class="text-sm font-semibold text-slate-700 font-sans">Hours Summary by Project</h3>
|
| 207 |
</div>
|
| 208 |
<table class="w-full">
|
| 209 |
<thead>
|
| 210 |
<tr class="border-b border-slate-100 bg-slate-50/60">
|
| 211 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Project</th>
|
| 212 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Total Tasks</th>
|
| 213 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Completed</th>
|
| 214 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Est. Hours</th>
|
| 215 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Completed Hrs</th>
|
| 216 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Completion %</th>
|
| 217 |
</tr>
|
| 218 |
</thead>
|
| 219 |
<tbody class="divide-y divide-slate-100">
|
| 220 |
+
@if (reportData.ProjectSummary.Any())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
{
|
| 222 |
+
@foreach (var proj in reportData.ProjectSummary)
|
| 223 |
{
|
| 224 |
+
var pct = proj.CompletionPercentage;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
<tr class="hover:bg-slate-50/50 transition-colors">
|
| 226 |
<td class="px-6 py-4">
|
| 227 |
<div class="flex items-center gap-2">
|
| 228 |
<i data-lucide="folder-kanban" class="h-4 w-4 text-violet-500"></i>
|
| 229 |
+
<span class="text-sm font-semibold text-slate-900">@proj.ProjectName</span>
|
| 230 |
</div>
|
| 231 |
</td>
|
| 232 |
+
<td class="px-6 py-4 text-sm text-slate-700 font-medium font-sans">@proj.TotalTasks</td>
|
| 233 |
+
<td class="px-6 py-4 text-sm text-emerald-600 font-medium font-sans">@proj.CompletedTasks</td>
|
| 234 |
+
<td class="px-6 py-4 text-sm font-semibold text-violet-700 font-sans">@proj.EstHours.ToString("F1") h</td>
|
| 235 |
+
<td class="px-6 py-4 text-sm font-semibold text-amber-600 font-sans">@proj.CompletedHours.ToString("F1") h</td>
|
| 236 |
<td class="px-6 py-4">
|
| 237 |
<div class="flex items-center gap-3">
|
| 238 |
<div class="flex-1 h-2 bg-slate-100 rounded-full overflow-hidden">
|
| 239 |
<div class="h-2 rounded-full @(pct >= 80 ? "bg-emerald-500" : pct >= 50 ? "bg-amber-400" : "bg-rose-400") transition-all"
|
| 240 |
style="width: @pct.ToString("F0")%"></div>
|
| 241 |
</div>
|
| 242 |
+
<span class="text-xs font-semibold text-slate-600 w-10 font-sans">@pct.ToString("F0")%</span>
|
| 243 |
</div>
|
| 244 |
</td>
|
| 245 |
</tr>
|
|
|
|
| 259 |
{
|
| 260 |
<div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
| 261 |
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
|
| 262 |
+
<h3 class="text-sm font-semibold text-slate-700 font-sans">
|
| 263 |
+
Task-level Detail — <span class="text-violet-600 font-sans">@FilteredDetailTasks.Count() task(s)</span>
|
| 264 |
</h3>
|
| 265 |
</div>
|
| 266 |
<table class="w-full">
|
| 267 |
<thead>
|
| 268 |
<tr class="border-b border-slate-100 bg-slate-50/60">
|
| 269 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Task</th>
|
| 270 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Project</th>
|
| 271 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Assignee</th>
|
| 272 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Est. Hours</th>
|
| 273 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Logged Hours</th>
|
| 274 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Due Date</th>
|
| 275 |
+
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Status</th>
|
| 276 |
</tr>
|
| 277 |
</thead>
|
| 278 |
<tbody class="divide-y divide-slate-100">
|
|
|
|
| 280 |
{
|
| 281 |
@foreach (var task in PagedDetailTasks)
|
| 282 |
{
|
|
|
|
|
|
|
| 283 |
var (statusLabel, statusClass) = task.StatusId switch {
|
| 284 |
2 => ("In Progress", "bg-blue-50 text-blue-700"),
|
| 285 |
3 => ("Done", "bg-emerald-50 text-emerald-700"),
|
|
|
|
| 289 |
<td class="px-6 py-4">
|
| 290 |
<p class="text-sm font-semibold text-slate-900">@task.Title</p>
|
| 291 |
</td>
|
| 292 |
+
<td class="px-6 py-4 text-sm text-slate-600 font-sans">@task.ProjectName</td>
|
| 293 |
<td class="px-6 py-4">
|
| 294 |
+
@if (!string.IsNullOrEmpty(task.AssigneeName))
|
| 295 |
{
|
| 296 |
<div class="flex items-center gap-2">
|
| 297 |
<span class="flex h-7 w-7 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold">
|
| 298 |
+
@(task.AssigneeName.Length > 0 ? task.AssigneeName[0].ToString().ToUpper() : "")
|
| 299 |
</span>
|
| 300 |
+
<span class="text-sm text-slate-700 font-sans">@task.AssigneeName</span>
|
| 301 |
</div>
|
| 302 |
}
|
| 303 |
+
else { <span class="text-xs text-slate-400 font-sans">Unassigned</span> }
|
| 304 |
</td>
|
| 305 |
<td class="px-6 py-4">
|
| 306 |
@if (task.EstimatedHours.HasValue)
|
| 307 |
{
|
| 308 |
+
<span class="text-sm font-semibold text-violet-700 font-sans">@task.EstimatedHours.Value.ToString("F1") h</span>
|
| 309 |
}
|
| 310 |
else
|
| 311 |
{
|
| 312 |
+
<span class="text-xs text-slate-400 font-sans">—</span>
|
| 313 |
}
|
| 314 |
</td>
|
|
|
|
| 315 |
<td class="px-6 py-4">
|
| 316 |
+
<span class="text-sm font-semibold text-amber-600 font-sans">@task.CompletedHours.ToString("F1") h</span>
|
| 317 |
+
</td>
|
| 318 |
+
<td class="px-6 py-4 text-sm text-slate-600 font-sans">@task.DueDate.ToString("dd-MMM-yyyy")</td>
|
| 319 |
+
<td class="px-6 py-4">
|
| 320 |
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @statusClass font-sans">@statusLabel</span>
|
| 321 |
</td>
|
| 322 |
</tr>
|
| 323 |
}
|
| 324 |
}
|
| 325 |
else
|
| 326 |
{
|
| 327 |
+
<EmptyState ColSpan="7" Icon="clock" Title="No tasks found" Subtitle="Try adjusting your filters." />
|
| 328 |
}
|
| 329 |
</tbody>
|
| 330 |
</table>
|
|
|
|
| 345 |
private bool isLoading = true;
|
| 346 |
private string currentView = "user";
|
| 347 |
|
| 348 |
+
private TimeTrackingReportDto reportData = new();
|
| 349 |
private List<UserDto> allUsers = new();
|
| 350 |
private List<ProjectDto> allProjects = new();
|
| 351 |
|
|
|
|
| 361 |
private int detailPage = 1;
|
| 362 |
private int pageSize = 10;
|
| 363 |
|
| 364 |
+
private IEnumerable<TaskTimeDetailDto> FilteredDetailTasks => reportData.TaskDetail;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
private int DetailTotalPages => (int)Math.Ceiling((double)FilteredDetailTasks.Count() / pageSize);
|
| 366 |
+
private IEnumerable<TaskTimeDetailDto> PagedDetailTasks => FilteredDetailTasks.Skip((detailPage - 1) * pageSize).Take(pageSize);
|
| 367 |
|
| 368 |
protected override async Task OnInitializedAsync()
|
| 369 |
{
|
| 370 |
var client = ApiClient.CreateClient();
|
| 371 |
try
|
| 372 |
{
|
|
|
|
| 373 |
var usersTask = client.GetFromJsonAsync<List<UserDto>>("User", Serialization.CaseInsensitive);
|
| 374 |
var projectsTask = client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive);
|
| 375 |
+
await System.Threading.Tasks.Task.WhenAll(usersTask, projectsTask);
|
| 376 |
|
|
|
|
| 377 |
allUsers = usersTask.Result ?? new();
|
| 378 |
allProjects = projectsTask.Result ?? new();
|
| 379 |
|
| 380 |
+
await LoadTimeTrackingData();
|
| 381 |
}
|
| 382 |
catch (Exception ex) { Console.WriteLine($"Load error: {ex.Message}"); }
|
| 383 |
finally { isLoading = false; }
|
|
|
|
| 388 |
await JS.InvokeVoidAsync("initIcons");
|
| 389 |
}
|
| 390 |
|
| 391 |
+
private async Task LoadTimeTrackingData()
|
| 392 |
{
|
| 393 |
+
isLoading = true;
|
| 394 |
+
StateHasChanged();
|
| 395 |
+
var client = ApiClient.CreateClient();
|
| 396 |
+
try
|
| 397 |
+
{
|
| 398 |
+
var query = $"Report/time-tracking?search={Uri.EscapeDataString(appliedSearch)}&projectId={appliedProjectId}&statusId={appliedStatusId}";
|
| 399 |
+
var result = await client.GetFromJsonAsync<Result<TimeTrackingReportDto>>(query, Serialization.CaseInsensitive);
|
| 400 |
+
if (result?.IsSuccess == true && result.Value != null)
|
| 401 |
+
{
|
| 402 |
+
reportData = result.Value;
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
catch (Exception ex)
|
| 406 |
+
{
|
| 407 |
+
Console.WriteLine($"Error loading time tracking data: {ex.Message}");
|
| 408 |
+
}
|
| 409 |
+
finally
|
| 410 |
+
{
|
| 411 |
+
isLoading = false;
|
| 412 |
+
StateHasChanged();
|
| 413 |
+
}
|
| 414 |
}
|
| 415 |
|
| 416 |
+
private async Task ApplyFilters()
|
| 417 |
{
|
| 418 |
appliedSearch = searchInput;
|
| 419 |
appliedProjectId = filterProjectId;
|
| 420 |
appliedStatusId = filterStatusId;
|
| 421 |
detailPage = 1;
|
| 422 |
+
await LoadTimeTrackingData();
|
| 423 |
}
|
| 424 |
|
| 425 |
+
private async Task ResetFilters()
|
| 426 |
{
|
| 427 |
searchInput = ""; appliedSearch = "";
|
| 428 |
filterProjectId = 0; appliedProjectId = 0;
|
| 429 |
filterStatusId = 0; appliedStatusId = 0;
|
| 430 |
detailPage = 1;
|
| 431 |
+
await LoadTimeTrackingData();
|
| 432 |
}
|
| 433 |
|
| 434 |
private void HandleDetailPageChanged(int page) => detailPage = page;
|
|
|
|
| 437 |
private async Task DownloadCsv()
|
| 438 |
{
|
| 439 |
var sb = new StringBuilder();
|
| 440 |
+
sb.AppendLine("Task Title,Project,Assignee,Estimated Hours,Logged Hours,Due Date,Status");
|
| 441 |
+
foreach (var task in reportData.TaskDetail)
|
| 442 |
{
|
| 443 |
+
var status = task.StatusId switch { 2 => "In Progress", 3 => "Done", _ => "To Do" };
|
| 444 |
+
sb.AppendLine($"\"{task.Title}\",\"{task.ProjectName}\",\"{task.AssigneeName}\",{task.EstimatedHours?.ToString() ?? ""},{task.CompletedHours},\"{task.DueDate:dd-MMM-yyyy}\",{status}");
|
|
|
|
|
|
|
| 445 |
}
|
| 446 |
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
| 447 |
var base64 = Convert.ToBase64String(bytes);
|
TaskTrackingSystem.sln
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
|
| 2 |
Microsoft Visual Studio Solution File, Format Version 12.00
|
| 3 |
-
# Visual Studio Version
|
| 4 |
-
VisualStudioVersion =
|
| 5 |
MinimumVisualStudioVersion = 10.0.40219.1
|
| 6 |
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskTrackingSystem.WebApi", "TaskTrackingSystem.WebApi\TaskTrackingSystem.WebApi.csproj", "{45C2E476-C820-4461-A5A3-31AF6A189839}"
|
| 7 |
EndProject
|
|
@@ -11,6 +11,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskTrackingSystem.Shared",
|
|
| 11 |
EndProject
|
| 12 |
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskTrackingSystem.WebApp", "TaskTrackingSystem.WebApp\TaskTrackingSystem.WebApp.csproj", "{6C65AAE1-991F-4E42-AFB9-B8BBA9D80A88}"
|
| 13 |
EndProject
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
Global
|
| 15 |
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
| 16 |
Debug|Any CPU = Debug|Any CPU
|
|
|
|
| 1 |
|
| 2 |
Microsoft Visual Studio Solution File, Format Version 12.00
|
| 3 |
+
# Visual Studio Version 18
|
| 4 |
+
VisualStudioVersion = 18.7.11903.348 stable
|
| 5 |
MinimumVisualStudioVersion = 10.0.40219.1
|
| 6 |
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskTrackingSystem.WebApi", "TaskTrackingSystem.WebApi\TaskTrackingSystem.WebApi.csproj", "{45C2E476-C820-4461-A5A3-31AF6A189839}"
|
| 7 |
EndProject
|
|
|
|
| 11 |
EndProject
|
| 12 |
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskTrackingSystem.WebApp", "TaskTrackingSystem.WebApp\TaskTrackingSystem.WebApp.csproj", "{6C65AAE1-991F-4E42-AFB9-B8BBA9D80A88}"
|
| 13 |
EndProject
|
| 14 |
+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{020D852B-0CDE-4177-87A2-6804A3043186}"
|
| 15 |
+
ProjectSection(SolutionItems) = preProject
|
| 16 |
+
README.md = README.md
|
| 17 |
+
EndProjectSection
|
| 18 |
+
EndProject
|
| 19 |
Global
|
| 20 |
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
| 21 |
Debug|Any CPU = Debug|Any CPU
|