User commited on
Commit
3bd1468
·
1 Parent(s): 3418204

add: Timelog table, BudgetedHours, estimated hours

Browse files
Files changed (37) hide show
  1. README.md +5 -0
  2. TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs +94 -119
  3. TaskTrackingSystem.Database/AppDbContextModels/Comment.cs +1 -1
  4. TaskTrackingSystem.Database/AppDbContextModels/FileAttachment.cs +1 -1
  5. TaskTrackingSystem.Database/AppDbContextModels/MenuAdmin.cs +19 -21
  6. TaskTrackingSystem.Database/AppDbContextModels/MenuAdminDetail.cs +17 -20
  7. TaskTrackingSystem.Database/AppDbContextModels/Project.cs +3 -1
  8. TaskTrackingSystem.Database/AppDbContextModels/Role.cs +1 -3
  9. TaskTrackingSystem.Database/AppDbContextModels/RoleMenu.cs +18 -16
  10. TaskTrackingSystem.Database/AppDbContextModels/Task.cs +3 -1
  11. TaskTrackingSystem.Database/AppDbContextModels/TimeLog.cs +33 -0
  12. TaskTrackingSystem.Database/AppDbContextModels/User.cs +4 -2
  13. TaskTrackingSystem.Shared/Models/Project/CreateProjectDto.cs +2 -0
  14. TaskTrackingSystem.Shared/Models/Project/ProjectDto.cs +1 -0
  15. TaskTrackingSystem.Shared/Models/Project/UpdateProjectDto.cs +2 -0
  16. TaskTrackingSystem.Shared/Models/Report/TaskReportDto.cs +1 -1
  17. TaskTrackingSystem.Shared/Models/Report/TimeTrackingReportDto.cs +59 -0
  18. TaskTrackingSystem.Shared/Models/Report/UserProductivityDto.cs +1 -0
  19. TaskTrackingSystem.Shared/Models/Task/CreateTaskDto.cs +1 -1
  20. TaskTrackingSystem.Shared/Models/Task/TaskDto.cs +2 -1
  21. TaskTrackingSystem.Shared/Models/Task/UpdateTaskDto.cs +1 -1
  22. TaskTrackingSystem.WebApi/Features/Project/ProjectService.cs +17 -5
  23. TaskTrackingSystem.WebApi/Features/Report/ReportController.cs +12 -0
  24. TaskTrackingSystem.WebApi/Features/Report/ReportService.cs +165 -8
  25. TaskTrackingSystem.WebApi/Features/Role/RoleController.cs +0 -21
  26. TaskTrackingSystem.WebApi/Features/Role/RoleService.cs +0 -68
  27. TaskTrackingSystem.WebApi/Features/Task/TaskService.cs +23 -4
  28. TaskTrackingSystem.WebApp/Components/Pages/EmployeeReport.razor +22 -4
  29. TaskTrackingSystem.WebApp/Components/Pages/OverdueTasksReport.razor +120 -0
  30. TaskTrackingSystem.WebApp/Components/Pages/ProjectAssign.razor +10 -1
  31. TaskTrackingSystem.WebApp/Components/Pages/ProjectProgressReport.razor +72 -2
  32. TaskTrackingSystem.WebApp/Components/Pages/Projects.razor +11 -2
  33. TaskTrackingSystem.WebApp/Components/Pages/TaskAssign.razor +10 -1
  34. TaskTrackingSystem.WebApp/Components/Pages/TaskReport.razor +16 -5
  35. TaskTrackingSystem.WebApp/Components/Pages/Tasks.razor +1 -1
  36. TaskTrackingSystem.WebApp/Components/Pages/TimeTrackingReport.razor +135 -170
  37. 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<Permission> Permissions { get; set; }
 
 
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<RolePermission> RolePermissions { get; set; }
33
 
34
  public virtual DbSet<Task> Tasks { get; set; }
35
 
36
  public virtual DbSet<TaskHistory> TaskHistories { get; set; }
37
 
38
- public virtual DbSet<User> Users { get; set; }
39
 
40
- public virtual DbSet<RoleMenu> RoleMenus { get; set; }
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
- if (!optionsBuilder.IsConfigured)
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.HasKey(e => e.Id).HasName("PK__AuditLog__3214EC07894BB819");
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("FK__AuditLogs__UserI__06CD04F7");
74
  });
75
 
76
  modelBuilder.Entity<Comment>(entity =>
77
  {
78
- entity.HasKey(e => e.Id).HasName("PK__Comments__3214EC0759C18B1B");
 
 
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("FK__Comments__TaskId__7C4F7684");
89
 
90
  entity.HasOne(d => d.User).WithMany(p => p.Comments)
91
  .HasForeignKey(d => d.UserId)
92
  .OnDelete(DeleteBehavior.ClientSetNull)
93
- .HasConstraintName("FK__Comments__UserId__7D439ABD");
94
  });
95
 
96
  modelBuilder.Entity<FileAttachment>(entity =>
97
  {
98
- entity.HasKey(e => e.Id).HasName("PK__FileAtta__3214EC070C2F3CA2");
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("FK__FileAttac__TaskI__02084FDA");
112
  });
113
 
114
- modelBuilder.Entity<Permission>(entity =>
115
  {
116
- entity.HasKey(e => e.Id).HasName("PK__Permissi__3214EC07C57993E8");
117
 
118
- entity.HasIndex(e => e.Name, "UQ__Permissi__737584F6C5746589").IsUnique();
 
 
 
 
 
 
 
 
119
 
120
- entity.Property(e => e.CreatedAt)
121
- .HasDefaultValueSql("(getdate())")
122
- .HasColumnType("datetime");
123
- entity.Property(e => e.Description).HasMaxLength(200);
124
- entity.Property(e => e.IsDeleted).HasDefaultValue(false);
125
- entity.Property(e => e.Module).HasMaxLength(50);
126
- entity.Property(e => e.Name).HasMaxLength(100);
127
- entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
 
128
  });
129
 
130
  modelBuilder.Entity<Project>(entity =>
131
  {
132
- entity.HasKey(e => e.Id).HasName("PK__Projects__3214EC072A5DAE2F");
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 }).HasName("PK__ProjectM__A76232349193C33E");
 
 
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("FK__ProjectMe__Proje__6EF57B66");
163
 
164
  entity.HasOne(d => d.User).WithMany(p => p.ProjectMembers)
165
  .HasForeignKey(d => d.UserId)
166
- .HasConstraintName("FK__ProjectMe__UserI__6FE99F9F");
167
  });
168
 
169
  modelBuilder.Entity<Role>(entity =>
170
  {
171
- entity.HasKey(e => e.Id).HasName("PK__Roles__3214EC07269DAA6F");
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<RolePermission>(entity =>
185
  {
186
- entity.HasKey(e => new { e.RoleId, e.PermissionId }).HasName("PK__RolePerm__6400A1A84E655E36");
187
-
188
- entity.Property(e => e.CreatedAt)
189
- .HasDefaultValueSql("(getdate())")
190
- .HasColumnType("datetime");
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.HasKey(e => e.Id).HasName("PK__Tasks__3214EC074B47FE29");
 
 
 
 
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.IsDeleted).HasDefaultValue(false);
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("FK__Tasks__ProjectId__73BA3083");
226
  });
227
 
228
  modelBuilder.Entity<TaskHistory>(entity =>
229
  {
230
- entity.HasKey(e => e.Id).HasName("PK__TaskHist__3214EC07A77801AE");
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("FK__TaskHisto__Modif__0B91BA14");
246
 
247
  entity.HasOne(d => d.Task).WithMany(p => p.TaskHistories)
248
  .HasForeignKey(d => d.TaskId)
249
- .HasConstraintName("FK__TaskHisto__TaskI__0A9D95DB");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  });
251
 
252
  modelBuilder.Entity<User>(entity =>
253
  {
254
- entity.HasKey(e => e.Id).HasName("PK__Users__3214EC07F132A452");
255
 
256
- entity.HasIndex(e => e.Username, "UQ__Users__536C85E43F85AAB5").IsUnique();
257
 
258
- entity.HasIndex(e => e.Email, "UQ__Users__A9D10534D2661DAC").IsUnique();
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("FK__Users__RoleId__6383C8BA");
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? IsDeleted { get; set; }
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? IsDeleted { get; set; }
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 partial class MenuAdmin
10
- {
11
- public string AdminMenuId { get; set; } = null!;
12
 
13
- public string MenuCode { get; set; } = null!;
14
 
15
- public string ParentCode { get; set; } = null!;
16
 
17
- public string MenuName { get; set; } = null!;
18
 
19
- public string? MenuUrl { get; set; }
20
 
21
- public bool Visible { get; set; }
22
 
23
- public int OrderNo { get; set; }
24
 
25
- public string? Icon { get; set; }
26
 
27
- public int DelFlag { get; set; }
28
 
29
- public string? CreatedUserId { get; set; }
30
- public DateTime CreatedDateTime { get; set; }
31
- public string? ModifiedUserId { get; set; }
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 partial class MenuAdminDetail
10
- {
11
- public string MenuAdminDetailId { get; set; } = null!;
12
 
13
- public string MenuDetailCode { get; set; } = null!;
14
 
15
- public string ParentMenuCode { get; set; } = null!;
16
 
17
- public string ActionName { get; set; } = null!;
18
 
19
- public string ApiName { get; set; } = null!;
20
 
21
- public bool Visible { get; set; }
22
 
23
- public int OrderNo { get; set; }
24
 
25
- public int DelFlag { get; set; }
26
 
27
- public string? CreatedUserId { get; set; }
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? IsDeleted { get; set; }
 
 
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
- using System;
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 partial class RoleMenu
10
- {
11
- public string RoleMenuId { get; set; } = null!;
12
- public string RoleCode { get; set; } = null!;
13
- public string MenuCode { get; set; } = null!;
14
- public int DelFlag { get; set; }
15
-
16
- public string? CreatedUserId { get; set; }
17
- public DateTime CreatedDateTime { get; set; }
18
- public string? ModifiedUserId { get; set; }
19
- public DateTime? ModifiedDateTime { get; set; }
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 int? EstimatedHours { get; set; }
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 int? EstimatedHours { get; set; }
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 int? EstimatedHours { get; set; }
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 int? EstimatedHours { get; set; }
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 int? EstimatedHours { get; set; }
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
- long? sid = sl switch { "to do" => 1, "in progress" => 2, "done" => 3, _ => null };
43
- if (sid.HasValue) query = query.Where(t => t.StatusId == sid.Value);
44
- else if (long.TryParse(status, out var pid)) query = query.Where(t => t.StatusId == pid);
 
 
 
 
 
 
 
45
  }
46
 
47
- var tasks = await query.ToListAsync();
 
 
 
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.Where(t => t.AssignedTo == user.Id && t.IsDeleted != true).ToListAsync();
76
- int total = tasks.Count, done = tasks.Count(t => t.StatusId == 3);
 
 
 
 
 
 
 
 
 
 
 
 
 
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.OrderByDescending(t => t.CreatedAt).ToListAsync();
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="4" class="px-6 py-12 text-center">
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
- roles = rolesTask.Result ?? new();
 
 
 
 
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 < DateTime.Today && t.StatusId != 3).ToList();
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
- roles = rolesTask.Result ?? new();
 
 
 
 
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-12 text-center">
277
- <i data-lucide="file-search" class="h-10 w-10 mx-auto text-slate-300 mb-3"></i>
278
- <p class="text-sm text-slate-400">No tasks found matching criteria.</p>
 
 
 
 
 
 
 
 
 
 
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 = null;
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 int? formEstimatedHours;
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 &amp; 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">@totalEstimated</p>
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">@tasksWithHours</p>
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">@avgHoursPerTask</p>
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">@completedHours</p>
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">Completion %</th>
 
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 group in userGroups)
163
  {
164
- var user = allUsers.FirstOrDefault(u => u.Id == group.Key);
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
- @if (user != null)
173
- {
174
- <div class="flex items-center gap-3">
175
- <span class="flex h-9 w-9 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold">
176
- @(user.FirstName.Length > 0 ? user.FirstName[0].ToString().ToUpper() : "")@(user.LastName.Length > 0 ? user.LastName[0].ToString().ToUpper() : "")
177
- </span>
178
- <div>
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
- else
185
- {
186
- <span class="text-sm text-slate-400 italic">Unassigned</span>
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="6" Icon="clock" Title="No timesheet data" Subtitle="No tasks found with the current filters." />
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 group in projGroups)
242
  {
243
- var project = allProjects.FirstOrDefault(p => p.Id == group.Key);
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">@(project?.Name ?? "Unknown Project")</span>
254
  </div>
255
  </td>
256
- <td class="px-6 py-4 text-sm text-slate-700 font-medium">@totalTasks</td>
257
- <td class="px-6 py-4 text-sm text-emerald-600 font-medium">@completedTasks</td>
258
- <td class="px-6 py-4 text-sm font-semibold text-violet-700">@estHours h</td>
259
- <td class="px-6 py-4 text-sm font-semibold text-amber-600">@compHours h</td>
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">Due Date</th>
298
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
 
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">@(project?.Name ?? "—")</td>
318
  <td class="px-6 py-4">
319
- @if (assignee != null)
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
- @(assignee.FirstName[0..1].ToUpper())@(assignee.LastName[0..1].ToUpper())
324
  </span>
325
- <span class="text-sm text-slate-700">@assignee.FirstName @assignee.LastName</span>
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="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @statusClass">@statusLabel</span>
 
 
 
 
343
  </td>
344
  </tr>
345
  }
346
  }
347
  else
348
  {
349
- <EmptyState ColSpan="6" Icon="clock" Title="No tasks found" Subtitle="Try adjusting your filters." />
350
  }
351
  </tbody>
352
  </table>
@@ -367,7 +345,7 @@
367
  private bool isLoading = true;
368
  private string currentView = "user";
369
 
370
- private List<TaskDto> allTasks = new();
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
- // Summary stats
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<TaskDto> PagedDetailTasks => FilteredDetailTasks.Skip((detailPage - 1) * pageSize).Take(pageSize);
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(tasksTask, usersTask, projectsTask);
426
 
427
- allTasks = tasksTask.Result ?? new();
428
  allUsers = usersTask.Result ?? new();
429
  allProjects = projectsTask.Result ?? new();
430
 
431
- ComputeSummary();
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 void ComputeSummary()
443
  {
444
- var withHours = allTasks.Where(t => t.EstimatedHours.HasValue).ToList();
445
- tasksWithHours = withHours.Count;
446
- totalEstimated = withHours.Sum(t => t.EstimatedHours ?? 0);
447
- avgHoursPerTask = tasksWithHours > 0 ? totalEstimated / tasksWithHours : 0;
448
- completedHours = allTasks.Where(t => t.StatusId == 3 && t.EstimatedHours.HasValue).Sum(t => t.EstimatedHours ?? 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  }
450
 
451
- private void ApplyFilters()
452
  {
453
  appliedSearch = searchInput;
454
  appliedProjectId = filterProjectId;
455
  appliedStatusId = filterStatusId;
456
  detailPage = 1;
 
457
  }
458
 
459
- private void ResetFilters()
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 GetFilteredTasks())
475
  {
476
- var assignee = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo);
477
- var project = allProjects.FirstOrDefault(p => p.Id == task.ProjectId);
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 &amp; 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 17
4
- VisualStudioVersion = 17.14.37301.10 d17.14
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