diff --git a/PPMTool.Data/Entities/Person.cs b/PPMTool.Data/Entities/Person.cs
index 7823693e..63d049c5 100644
--- a/PPMTool.Data/Entities/Person.cs
+++ b/PPMTool.Data/Entities/Person.cs
@@ -308,5 +308,23 @@ public double GetWorkloadModelTotalOnDate(DateTime date)
.OrderBy(x => x.ChangeDate)
.LastOrDefault();
}
+
+ ///
+ /// Shadow property to store a comma-separated list of the person's skill tag names.
+ /// Used for filtering on the Manage People page.
+ ///
+ [NotMapped]
+ public string SkillTags
+ {
+ get => string.Join(", ", OwnedSkills.Select(skill => skill.SkillTag.Name).OrderBy(name => name));
+ }
+
+ ///
+ /// Shadow property to store the current grade of the person based on the person's current WLM and the date.
+ /// Used for filtering by Grade on the Manage People page currently.
+ ///
+ ///
+ [NotMapped]
+ public int? CurrentGrade { get => GetGradeOnDate(DateTime.Today); }
}
}
diff --git a/PPMTool.Data/Entities/Project.cs b/PPMTool.Data/Entities/Project.cs
index c1fbe84d..ab1a1e77 100644
--- a/PPMTool.Data/Entities/Project.cs
+++ b/PPMTool.Data/Entities/Project.cs
@@ -136,6 +136,14 @@ public class Project : BaseTask, ILoggableObject
///
public virtual ICollection FundingSources { get; set; } = new List();
+ ///
+ /// Shadow property to allow easy filtering on FundsReceived in datagrids.
+ /// Requires additional logic in the service layer to populate this property when retrieving projects from the database.
+ ///
+ ///
+ [NotMapped]
+ public double FundsReceived { get; set; }
+
///
/// Constructor also adds default status messages
///
diff --git a/PPMTool.Data/Entities/Setting.cs b/PPMTool.Data/Entities/Setting.cs
index fe2a46d4..d1ac2fbf 100644
--- a/PPMTool.Data/Entities/Setting.cs
+++ b/PPMTool.Data/Entities/Setting.cs
@@ -23,7 +23,8 @@ public class Setting : ILoggableObject
public SettingType SettingType { get; set; }
///
- /// Shadow property to get the name of the setting type as a string for use in the UI. This is not mapped to the database as it is derived from the SettingType enum.
+ /// Shadow property to get the name of the setting type as a string for use in the UI.
+ /// This is not mapped to the database as it is derived from the SettingType enum.
///
[NotMapped]
public string SettingTypeAsText => SettingType.ToString();
diff --git a/PPMTool.Data/Entities/TimesheetEntry.cs b/PPMTool.Data/Entities/TimesheetEntry.cs
index 53903ad5..704d6928 100644
--- a/PPMTool.Data/Entities/TimesheetEntry.cs
+++ b/PPMTool.Data/Entities/TimesheetEntry.cs
@@ -73,19 +73,20 @@ public class TimesheetEntry : ILoggableObject
public double SundayHours { get; set; }
///
- /// Total hours for the week for this entry
+ /// Total hours for the week for this entry. This must be explicitly updated with the method.
///
[NotMapped]
public double TotalHours { get; private set; }
///
- /// Represents whether this task is part of the user's template
+ /// Represents whether this task is part of the user's template. This must be explicitly set with the method.
///
[NotMapped]
- public bool IsInTemplate { get; set; }
+ public bool IsInTemplate { get; private set; }
///
- /// Method to sum up the hours in the entry
+ /// Method to sum up the hours in the entry and update the TotalHours property.
+ /// This should be called whenever the hours for any day are modified.
///
///
public void UpdateTotalHours()
@@ -93,6 +94,15 @@ public void UpdateTotalHours()
TotalHours = MondayHours + TuesdayHours + WednesdayHours + ThursdayHours + FridayHours + SaturdayHours + SundayHours;
}
+ ///
+ /// Sets the IsInTemplate property to indicate whether this task is part of the user's template.
+ ///
+ ///
+ public void SetIsInTemplate(bool isInTemplate)
+ {
+ IsInTemplate = isInTemplate;
+ }
+
///
/// Returns a useful string to identify the entity
///
diff --git a/PPMTool/Pages/AddTimesheet.razor.cs b/PPMTool/Pages/AddTimesheet.razor.cs
index 6f3a8e9b..5ff63ec2 100644
--- a/PPMTool/Pages/AddTimesheet.razor.cs
+++ b/PPMTool/Pages/AddTimesheet.razor.cs
@@ -234,7 +234,7 @@ private List OrderByTemplate(Timesheet timesheet, string templat
// Sets a boolean for use in the datagrid to show which items are part of the template
foreach (TimesheetEntry e in orderedResults)
{
- e.IsInTemplate = order.Contains(e.InnateCodeTask.InnateCodeTaskId);
+ e.SetIsInTemplate(order.Contains(e.InnateCodeTask.InnateCodeTaskId));
}
return orderedResults;
diff --git a/PPMTool/Pages/People.razor b/PPMTool/Pages/People.razor
index 52e01d8e..a048dbf0 100644
--- a/PPMTool/Pages/People.razor
+++ b/PPMTool/Pages/People.razor
@@ -64,7 +64,7 @@ SPDX-License-Identifier: apache-2.0
-
+
@if (p.LineManager?.IsCurrentlyAbsent() ?? false)
@@ -75,16 +75,16 @@ SPDX-License-Identifier: apache-2.0
-
-
-
+
+
+
@p.WorkloadModelChanges.Where(x => x.ChangeDate <= DateTime.Today).OrderBy(x => x.ChangeDate).LastOrDefault()?.ProjectWorkFTE
-
+
- @p.WorkloadModelChanges.Where(x => x.ChangeDate <= DateTime.Today).OrderBy(x => x.ChangeDate).LastOrDefault()?.Grade
+ @p.CurrentGrade
@if (skillsEnabled)
@@ -95,7 +95,7 @@ SPDX-License-Identifier: apache-2.0
-
+
@TagService.GetCountForPerson(Context, p.PersonId).ToString()
diff --git a/PPMTool/Pages/People.razor.cs b/PPMTool/Pages/People.razor.cs
index bd54972e..d26baa98 100644
--- a/PPMTool/Pages/People.razor.cs
+++ b/PPMTool/Pages/People.razor.cs
@@ -112,87 +112,45 @@ private void LoadData(LoadDataArgs args)
query = query.Where(x => ActiveUser.Person != null && x.PersonId == ActiveUser.Person!.PersonId);
}
- // Now apply the custom filters (pattern matching syntax for non-null object)
- if (args.Filters is { } filters && filters.Any())
+ // ---- GRID FILTERING ----
+ if (!string.IsNullOrWhiteSpace(args.Filter))
{
- // Filter on Skill Tags
- var filter = args.Filters.FirstOrDefault(x => x.Property == "SkillTags");
- var filterValue = filter?.FilterValue as string;
- if (filter != null && filterValue != null)
- {
- query = query.Where(x => x.OwnedSkills.Any(x => x.SkillTag.Name.Trim().ToLower().Contains(filterValue.Trim().ToLower())));
- }
+ query = query.Where(args.Filter);
+ }
+
+ // ---- SORTING ----
+ if (args.Sorts != null && args.Sorts.Count() > 0)
+ {
+ var sort = args.Sorts.First();
- // Add filter on Line Manager
- filter = filters.FirstOrDefault(f => f.Property == "LineManager");
- filterValue = (filter?.FilterValue as string)?.Trim();
- if (!string.IsNullOrEmpty(filterValue))
+ // Special-case sort
+ if (sort.Property == "SkillsCount")
{
- var filterValueLower = filterValue.ToLower();
- query = query.Where(x =>
- x.LineManager != null &&
- (x.LineManager.Name ?? "").Trim().ToLower().Contains(filterValueLower));
+ query = sort.SortOrder == SortOrder.Ascending
+ ? query.OrderBy(x => x.OwnedSkills.Count())
+ : query.OrderByDescending(x => x.OwnedSkills.Count());
}
-
- // Add any filters than are none of the special ones
- var stdFilters = args.Filters.Where(x => x.Property != "SkillTags" && x.Property != "LineManager");
- if (stdFilters.Any())
+ else
{
- // Filter via the Where method
- query = query.Where(args.Filter);
+ query = query.OrderBy(args.OrderBy);
}
}
-
- // Sorting needed
- if (!string.IsNullOrEmpty(args.OrderBy))
+ else if (!string.IsNullOrWhiteSpace(args.OrderBy))
{
- // Check details of the sort
- if (args.Sorts is { } sorts && sorts.Any())
- {
- var sort = args.Sorts?.First();
- if (sort.Property == "SkillsCount")
- {
- // Apply the ordering process on skills count manually
- if (sort.SortOrder == SortOrder.Ascending)
- {
- query = query.OrderBy(x => TagService.GetCountForPerson(Context, x.PersonId));
- }
- else
- {
- query = query.OrderByDescending(x => TagService.GetCountForPerson(Context, x.PersonId));
- }
- }
- else if (sort.Property == "LineManager")
- {
- query = sort.SortOrder == SortOrder.Ascending ?
- query.OrderBy(x => x.LineManager != null ? x.LineManager.Name : "") :
- query.OrderByDescending(x => x.LineManager != null ? x.LineManager.Name : "");
- }
-
- // No special handling required
- else
- {
- // Sort via the OrderBy method
- query = query.OrderBy(args.OrderBy);
- }
- }
+ query = query.OrderBy(args.OrderBy);
}
- // Important!!! Make sure the Count property of RadzenDataGrid is set.
- var data = query.ToList();
- count = data.Count();
+ // ---- COUNT BEFORE PAGING ----
+ count = query.Count();
- // Perform paging via Skip and Take.
- if (args.Skip == null)
- {
- people = data.Take(pageCount).ToList();
- }
- else
- {
- people = data.Skip(args.Skip.Value).Take(args.Top.Value).ToList();
- }
+ // ---- PAGING ----
+ var skip = args.Skip ?? 0;
+ var take = args.Top ?? pageCount;
+
+ people = query.Skip(skip).Take(take).ToList();
- Debug.WriteLine($"** {data.Count()} people loaded. {people.Count()} displayed.");
+ Debug.WriteLine($"** {count} people loaded. {people.Count()} displayed.");
}
+
}
}
\ No newline at end of file
diff --git a/PPMTool/Pages/Projects.razor b/PPMTool/Pages/Projects.razor
index 1ccabce3..385c2414 100644
--- a/PPMTool/Pages/Projects.razor
+++ b/PPMTool/Pages/Projects.razor
@@ -44,7 +44,7 @@ SPDX-License-Identifier: apache-2.0
-
+
@@ -71,18 +71,25 @@ SPDX-License-Identifier: apache-2.0
@p.ProjectStatus.ToNiceString()
-
+
@p.School.Faculty.Code
-
+
@p.School.Code
-
+
@if (p.StartDate == DateTime.MaxValue)
{
@@ -90,11 +97,18 @@ SPDX-License-Identifier: apache-2.0
}
else
{
- p.StartDate.ToString("d");
+ @p.StartDate.ToShortDateString()
}
-
+
@if (p.EndDate == DateTime.MinValue)
{
@@ -102,7 +116,7 @@ SPDX-License-Identifier: apache-2.0
}
else
{
- p.EndDate.ToString("d");
+ @p.EndDate.ToShortDateString()
}
@@ -113,20 +127,48 @@ SPDX-License-Identifier: apache-2.0
@(p.CostModel.GetAttribute().Name)
-
+
@(p.CostModel == CostModel.DayRate ? p.DayRate.ToString("C0") : "N/A")
-
-
-
+
+
+
- @(PaymentService.GetFundsReceived(Context, p.ProjectId).ToString("C0"))
+ @p.FundsReceived.ToString("C0")
}
-
+
diff --git a/PPMTool/Pages/Projects.razor.cs b/PPMTool/Pages/Projects.razor.cs
index 3ca10683..5fb64164 100644
--- a/PPMTool/Pages/Projects.razor.cs
+++ b/PPMTool/Pages/Projects.razor.cs
@@ -126,6 +126,7 @@ private void OnLoadData(LoadDataArgs args)
// Initialise the project list -- developers can only see projects to which they are assigned
IQueryable query = ProjectService.GetAll(Context).OrderBy(x => x.RTP).AsQueryable();
+
if (ActiveUserRoleType == RoleType.Developer)
{
query = query.Where(x => ActiveUser.Person != null && x.SubTasks.Any(x => x.AssignedResources.Any(x => x.Person.PersonId == ActiveUser.Person!.PersonId)));
@@ -141,46 +142,35 @@ private void OnLoadData(LoadDataArgs args)
query = query.Where(args.Filter);
}
- // Filter on Faculty and School
- if (args.Filters is { } filters && filters.Any())
- {
- var filter = filters.FirstOrDefault(f => f.Property == "Faculty");
- var filterValue = (filter?.FilterValue as string)?.Trim();
- if (!string.IsNullOrEmpty(filterValue))
- {
- var filterValueLower = filterValue.ToLower();
- query = query.Where(x =>
- x.School != null &&
- x.School.Faculty != null &&
- ((x.School.Faculty.Code ?? "").Trim().ToLower()).Contains(filterValueLower));
- }
-
- filter = filters.FirstOrDefault(f => f.Property == "School");
- filterValue = (filter?.FilterValue as string)?.Trim();
- if (!string.IsNullOrEmpty(filterValue))
- {
- var filterValueLower = filterValue.ToLower();
- query = query.Where(x =>
- x.School != null &&
- ((x.School.Code ?? "").Trim().ToLower()).Contains(filterValueLower));
- }
- }
-
query = ApplySorting(query, args);
-
// Assign to grid source
var data = query.ToList();
count = data.Count;
+
+ List projectsToDisplay;
if (args.Skip == null)
{
- projects = data.Take(pageCount).ToList();
+ projectsToDisplay = data.Take(pageCount).ToList();
}
else
{
- projects = data.Skip(args.Skip.Value).Take(args.Top.Value).ToList();
+ projectsToDisplay = data.Skip(args.Skip.Value).Take(args.Top.Value).ToList();
+ }
+
+ // Load FundsReceived for displayed projects only (after filtering and paging)
+ var projectIds = projectsToDisplay.Select(p => p.ProjectId).ToList();
+ var fundsReceivedLookup = PaymentService.GetFundsReceivedForProjects(Context, projectIds);
+
+ // Populate FundsReceived from the lookup
+ foreach (var project in projectsToDisplay)
+ {
+ project.FundsReceived = fundsReceivedLookup.TryGetValue(project.ProjectId, out var funds) ? funds : 0;
}
+ // Now assign to bound variable for display
+ projects = projectsToDisplay;
+
Debug.WriteLine($"** {data.Count()} projects loaded. {projects.Count()} displayed.");
}
diff --git a/PPMTool/Services/PaymentService.cs b/PPMTool/Services/PaymentService.cs
index cf00920d..daf18612 100644
--- a/PPMTool/Services/PaymentService.cs
+++ b/PPMTool/Services/PaymentService.cs
@@ -93,6 +93,26 @@ public double GetFundsReceived(PPMToolContext context, int projectId)
.RoundedSum(x => x.Value, 0);
}
+ ///
+ /// Gets funds received for multiple projects in a single query and return as a dictionary mapping ProjectId to FundsReceived.
+ ///
+ ///
+ ///
+ /// Dictionary mapping ProjectId to FundsReceived
+ public IDictionary GetFundsReceivedForProjects(PPMToolContext context, IEnumerable projectIds)
+ {
+ // Return an empty dictionary if the projectIds collection is null or empty
+ var projectIdList = projectIds.ToList();
+ if (!projectIdList.Any())
+ return new Dictionary();
+
+ return context.Payments
+ .Where(p => projectIdList.Contains(p.Project.ProjectId))
+ .GroupBy(p => p.Project.ProjectId)
+ .Select(g => new { ProjectId = g.Key, FundsReceived = g.Sum(p => p.Value) })
+ .ToDictionary(x => x.ProjectId, x => Math.Round(x.FundsReceived, 0));
+ }
+
///
/// Gets the invoice status by evaluating payments made against the invoice value
///