|
|
@@ -29,6 +29,7 @@ using Syncfusion.Data.Extensions;
|
|
|
using NPOI.OpenXmlFormats.Spreadsheet;
|
|
|
using Syncfusion.UI.Xaml.Scheduler;
|
|
|
using Color = System.Drawing.Color;
|
|
|
+using System.Diagnostics.CodeAnalysis;
|
|
|
|
|
|
namespace PRSDesktop;
|
|
|
|
|
|
@@ -87,7 +88,7 @@ public partial class JobResourcePlanner : UserControl
|
|
|
|
|
|
private EmployeeResourceModel[] _emps = new EmployeeResourceModel[] { };
|
|
|
|
|
|
- private List<AssignmentModel> _assignments = new List<AssignmentModel>();
|
|
|
+ private List<Assignment> _assignments = new List<Assignment>();
|
|
|
|
|
|
private DateTime[] _dates = Array.Empty<DateTime>();
|
|
|
private double[] _totals = Array.Empty<double>();
|
|
|
@@ -278,24 +279,56 @@ public partial class JobResourcePlanner : UserControl
|
|
|
{
|
|
|
using (new WaitCursor())
|
|
|
{
|
|
|
-
|
|
|
- var jobids = _jobs.Select(x => x.ID).ToArray();
|
|
|
- var empids = _emps.Select(x => x.ID).ToArray();
|
|
|
- var actids = _activities.Select(x => x.ID).ToArray();
|
|
|
+ var empids = _emps.ToArray(x => x.ID);
|
|
|
+ var jobIDs = _jobs.ToArray(x => x.ID);
|
|
|
|
|
|
DateTime fromDate = DateTime.Today;
|
|
|
DateTime toDate = DateTime.Today.AddMonths(Properties.MonthsToView);
|
|
|
-
|
|
|
- MultiQuery query = new MultiQuery();
|
|
|
|
|
|
- query.Add<Assignment>(
|
|
|
- Filter<Assignment>.Where(x =>x.Employee.ID).InList(empids)
|
|
|
- .And(x=>x.Date).IsGreaterThanOrEqualTo(fromDate)
|
|
|
- .And(x=>x.Date).IsLessThanOrEqualTo(toDate),
|
|
|
- AssignmentModel.Columns
|
|
|
- );
|
|
|
- query.Query();
|
|
|
- _assignments = query.Get<Assignment>().Rows.Select(r => new AssignmentModel(r)).ToList();
|
|
|
+ var query = Client.QueryMultiple(
|
|
|
+ new KeyedQueryDef<Assignment>(
|
|
|
+ Filter<Assignment>.Where(x => x.Employee.ID).InList(empids)
|
|
|
+ .And(x => x.Date).IsGreaterThanOrEqualTo(fromDate)
|
|
|
+ .And(x => x.Date).IsLessThanOrEqualTo(toDate)
|
|
|
+ .Or(Filter<Assignment>.Where(x => x.JobStage.Job.ID).InList(jobIDs)),
|
|
|
+ Columns.None<Assignment>()
|
|
|
+ .Add(x => x.Date)
|
|
|
+ .Add(x => x.Booked.Start)
|
|
|
+ .Add(x => x.Booked.Finish)
|
|
|
+ .Add(x => x.Booked.Duration)
|
|
|
+ .Add(x => x.Job.ID)
|
|
|
+ .Add(x => x.JobStage.ID)
|
|
|
+ .Add(x => x.Employee.ID)),
|
|
|
+ new KeyedQueryDef<Job>(
|
|
|
+ Filter<Job>.Where(x => x.ID).InList(_jobs.ToArray(x => x.ID)),
|
|
|
+ Columns.None<Job>()
|
|
|
+ .Add(x => x.ID)
|
|
|
+ .Add(x => x.WorkingDays)),
|
|
|
+ new KeyedQueryDef<CompanyInformation>(
|
|
|
+ Filter.All<CompanyInformation>(),
|
|
|
+ Columns.None<CompanyInformation>()
|
|
|
+ .Add(x => x.WorkingDays),
|
|
|
+ range: CoreRange.Database(1)),
|
|
|
+ new KeyedQueryDef<JobStage>(
|
|
|
+ Filter<JobStage>.Where(x => x.Job.ID).InList(jobIDs),
|
|
|
+ Columns.None<JobStage>()
|
|
|
+ .Add(x => x.ID)
|
|
|
+ .Add(x => x.Job.ID)
|
|
|
+ .Add(x => x.StartDate)
|
|
|
+ .Add(x => x.EndDate)
|
|
|
+ .Add(x => x.TotalHours)));
|
|
|
+
|
|
|
+ _assignments = query.GetList<Assignment>();
|
|
|
+
|
|
|
+ var jobWorkingHours = query.GetObjects<Job>()
|
|
|
+ .ToDictionary(x => x.ID, x =>
|
|
|
+ {
|
|
|
+ return WorkingDayHours.Deserialize(x.WorkingDays ?? "");
|
|
|
+ });
|
|
|
+ var jobStages = query
|
|
|
+ .GetObjects<JobStage>()
|
|
|
+ .GroupByDictionary(x => x.Job.ID);
|
|
|
+ var defaultWorkingHours = WorkingDayHours.Deserialize(query.GetObjects<CompanyInformation>().FirstOrDefault()?.WorkingDays ?? "");
|
|
|
|
|
|
var data = new DataTable();
|
|
|
|
|
|
@@ -310,6 +343,8 @@ public partial class JobResourcePlanner : UserControl
|
|
|
var available = new double[dates.Count];
|
|
|
var totals = new double[dates.Count];
|
|
|
|
|
|
+ var required = new decimal[dates.Count, _jobs.Length];
|
|
|
+
|
|
|
foreach (var (dateIdx, date) in dates.WithIndex())
|
|
|
{
|
|
|
double avail = 0.0F;
|
|
|
@@ -334,15 +369,15 @@ public partial class JobResourcePlanner : UserControl
|
|
|
var values = new List<object?> { date };
|
|
|
|
|
|
var anyjobstoday = _assignments.Where(x => (x.Date.Date == date.Date));
|
|
|
- avail -= anyjobstoday.Aggregate<AssignmentModel, double>(0F, (value, model) => value + model.BookedDuration.TotalHours);
|
|
|
+ avail -= anyjobstoday.Aggregate<Assignment, double>(0F, (value, model) => value + model.Booked.Duration.TotalHours);
|
|
|
|
|
|
foreach (var (jobIdx, job) in _jobs.WithIndex())
|
|
|
{
|
|
|
- var thisjobtoday = _assignments.Where(x => (x.Date.Date == date.Date) && (x.JobID == job.ID));
|
|
|
+ var thisjobtoday = _assignments.Where(x => (x.Date.Date == date.Date) && (x.Job.ID == job.ID));
|
|
|
if (thisjobtoday.Any())
|
|
|
{
|
|
|
- var assigned = thisjobtoday.Aggregate<AssignmentModel, double>(0F,
|
|
|
- (value, model) => value + model.BookedDuration.TotalHours);
|
|
|
+ var assigned = thisjobtoday.Aggregate<Assignment, double>(0F,
|
|
|
+ (value, model) => value + model.Booked.Duration.TotalHours);
|
|
|
dataValues[dateIdx, jobIdx] = assigned / Properties.HoursPerDay;
|
|
|
}
|
|
|
else
|
|
|
@@ -352,6 +387,86 @@ public partial class JobResourcePlanner : UserControl
|
|
|
available[dateIdx] = avail / Properties.HoursPerDay;
|
|
|
totals[dateIdx] = total / Properties.HoursPerDay;
|
|
|
}
|
|
|
+
|
|
|
+ foreach(var (jobID, stages) in jobStages)
|
|
|
+ {
|
|
|
+ var workingHours = jobWorkingHours.GetValueOrDefault(jobID) ?? defaultWorkingHours;
|
|
|
+ foreach(var stage in stages)
|
|
|
+ {
|
|
|
+ var totalRequiredHoursRemaining = stage.TotalHours;
|
|
|
+ var stageAssignments = _assignments.Where(x => x.JobStage.ID == stage.ID)
|
|
|
+ .GroupByDictionary(x => x.Date.Date);
|
|
|
+ foreach(var assignment in stageAssignments.SelectMany(x => x.Value))
|
|
|
+ {
|
|
|
+ if(assignment.Date.Date < DateTime.Today)
|
|
|
+ {
|
|
|
+ totalRequiredHoursRemaining -= assignment.Booked.Duration.TotalHoursDecimal();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var remainingDates = new HashSet<DateTime>();
|
|
|
+ for(var date = DateTime.Today; date <= stage.EndDate.Date; date = date.AddDays(1))
|
|
|
+ {
|
|
|
+ if (workingHours[(int)date.DayOfWeek].IsWorkingDay)
|
|
|
+ {
|
|
|
+ remainingDates.Add(date);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ var dayTotals = remainingDates.ToDictionary(
|
|
|
+ x => x,
|
|
|
+ x =>
|
|
|
+ {
|
|
|
+ if (stageAssignments.TryGetValue(x, out var dayAssignments))
|
|
|
+ {
|
|
|
+ return dayAssignments.Sum(x => x.Booked.Duration.TotalHoursDecimal());
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ while (totalRequiredHoursRemaining > 0 && remainingDates.Count > 0)
|
|
|
+ {
|
|
|
+ var averageHoursPerDay = totalRequiredHoursRemaining / remainingDates.Count;
|
|
|
+
|
|
|
+ var tempRemainingDates = new HashSet<DateTime>();
|
|
|
+ var changed = false;
|
|
|
+ foreach(var date in remainingDates)
|
|
|
+ {
|
|
|
+ var dayTotal = dayTotals.GetValueOrDefault(date);
|
|
|
+ if(dayTotal >= averageHoursPerDay)
|
|
|
+ {
|
|
|
+ totalRequiredHoursRemaining -= dayTotal;
|
|
|
+ changed = true;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ tempRemainingDates.Add(date);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ remainingDates = tempRemainingDates;
|
|
|
+ if (!changed) break;
|
|
|
+ }
|
|
|
+
|
|
|
+ var hoursPerDay =
|
|
|
+ remainingDates.Count > 0 ? Math.Max(0, totalRequiredHoursRemaining) / remainingDates.Count
|
|
|
+ : 0;
|
|
|
+ foreach(var date in remainingDates)
|
|
|
+ {
|
|
|
+ var dateIdx = dates.IndexOf(date);
|
|
|
+ if (dateIdx == -1) continue;
|
|
|
+
|
|
|
+ var jobEntry = _jobs.WithIndex().FirstOrDefault(x => x.Value.ID == jobID);
|
|
|
+ if (jobEntry.Value is null) continue;
|
|
|
+ var jobIdx = jobEntry.Key;
|
|
|
+
|
|
|
+ required[dateIdx, jobIdx] += hoursPerDay - dayTotals.GetValueOrDefault(date);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
_totals = totals;
|
|
|
_available = available;
|
|
|
|
|
|
@@ -813,8 +928,8 @@ public partial class JobResourcePlanner : UserControl
|
|
|
AdjustStandardLeave(date, ref rostered);
|
|
|
AdjustLeaveRequests(date, emp, ref rostered);
|
|
|
|
|
|
- var assignments = _assignments.Where(x => (x.Date == date) && (x.EmployeeID == emp.ID));
|
|
|
- var assigned = assignments.Aggregate(TimeSpan.Zero, (time, assign) => time += assign.BookedDuration);
|
|
|
+ var assignments = _assignments.Where(x => (x.Date == date) && (x.Employee.ID == emp.ID));
|
|
|
+ var assigned = assignments.Aggregate(TimeSpan.Zero, (time, assign) => time += assign.Booked.Duration);
|
|
|
|
|
|
if (rostered > assigned)
|
|
|
GetEmployee(emp.ID, availableemployees).Time += rostered.Subtract(assigned);
|
|
|
@@ -827,8 +942,8 @@ public partial class JobResourcePlanner : UserControl
|
|
|
private void LoadAssignedEmployees(Guid[] jobIDs, DateTime[] dates)
|
|
|
{
|
|
|
List<JobPlannerEmployee> assignedemployees = new List<JobPlannerEmployee>();
|
|
|
- foreach (var assignment in _assignments.Where(x => dates.Contains(x.Date) && jobIDs.Contains(x.JobID)))
|
|
|
- GetEmployee(assignment.EmployeeID, assignedemployees).Time += assignment.BookedDuration;
|
|
|
+ foreach (var assignment in _assignments.Where(x => dates.Contains(x.Date) && jobIDs.Contains(x.Job.ID)))
|
|
|
+ GetEmployee(assignment.Employee.ID, assignedemployees).Time += assignment.Booked.Duration;
|
|
|
AssignedEmployees.Items = assignedemployees.OrderBy(x=>x.Name).ToList();
|
|
|
AssignedEmployees.Refresh(false, true);
|
|
|
}
|
|
|
@@ -959,11 +1074,11 @@ public partial class JobResourcePlanner : UserControl
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- bool IsAssigned(AssignmentModel[] assignments, TimeSpan start, TimeSpan finish)
|
|
|
+ bool IsAssigned(Assignment[] assignments, TimeSpan start, TimeSpan finish)
|
|
|
{
|
|
|
foreach (var assignment in assignments)
|
|
|
{
|
|
|
- if ((assignment.BookedStart <= start) && (assignment.BookedFinish >= finish))
|
|
|
+ if ((assignment.Booked.Start <= start) && (assignment.Booked.Finish >= finish))
|
|
|
return true;
|
|
|
}
|
|
|
return false;
|
|
|
@@ -971,11 +1086,9 @@ public partial class JobResourcePlanner : UserControl
|
|
|
|
|
|
if (ExtractSelection(out var jobIDs, out var dates))
|
|
|
{
|
|
|
-
|
|
|
if (dataGrid.ItemsSource is DataTable table)
|
|
|
{
|
|
|
-
|
|
|
- List<Assignment> updates = new List<Assignment>();
|
|
|
+ var newAssignments = new List<Assignment>();
|
|
|
foreach (var available in availables)
|
|
|
{
|
|
|
foreach (var date in dates)
|
|
|
@@ -996,9 +1109,9 @@ public partial class JobResourcePlanner : UserControl
|
|
|
if (GetLeaveRequestTimes(date, emp, out TimeSpan leaverequeststart, out TimeSpan leaverequestfinish))
|
|
|
CheckEdges(leaverequeststart, leaverequestfinish);
|
|
|
|
|
|
- var assignments = _assignments.Where(x => (x.Date == date) && (x.EmployeeID == emp.ID)).ToArray();
|
|
|
+ var assignments = _assignments.Where(x => (x.Date == date) && (x.Employee.ID == emp.ID)).ToArray();
|
|
|
foreach (var assignment in assignments)
|
|
|
- CheckEdges(assignment.BookedStart, assignment.BookedFinish);
|
|
|
+ CheckEdges(assignment.Booked.Start, assignment.Booked.Finish);
|
|
|
edges.Sort();
|
|
|
|
|
|
var adjustment = new double[jobIDs.Length];
|
|
|
@@ -1014,7 +1127,7 @@ public partial class JobResourcePlanner : UserControl
|
|
|
{
|
|
|
foreach(var (idx, jobid) in jobIDs.WithIndex())
|
|
|
{
|
|
|
- Assignment assignment = new Assignment();
|
|
|
+ var assignment = new Assignment();
|
|
|
assignment.Activity.ID = Properties.ActivityType;
|
|
|
assignment.Employee.ID = emp.ID;
|
|
|
assignment.Date = date;
|
|
|
@@ -1022,7 +1135,7 @@ public partial class JobResourcePlanner : UserControl
|
|
|
assignment.Booked.Start = start;
|
|
|
assignment.Booked.Finish = finish;
|
|
|
assignment.Booked.Duration = finish - start;
|
|
|
- updates.Add(assignment);
|
|
|
+ newAssignments.Add(assignment);
|
|
|
adjustment[idx] += assignment.Booked.Duration.TotalHours;
|
|
|
}
|
|
|
}
|
|
|
@@ -1087,16 +1200,12 @@ public partial class JobResourcePlanner : UserControl
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (updates.Any())
|
|
|
+ if (newAssignments.Count != 0)
|
|
|
{
|
|
|
using (new WaitCursor())
|
|
|
{
|
|
|
- new Client<Assignment>().Save(updates, "Assigned by Job Planner");
|
|
|
-
|
|
|
- CoreTable temp = new CoreTable();
|
|
|
- temp.LoadColumns(typeof(Assignment));
|
|
|
- temp.LoadRows(updates);
|
|
|
- _assignments.AddRange(temp.Rows.Select(r => new AssignmentModel(r)));
|
|
|
+ Client.Save(newAssignments, "Assigned by Job Planner");
|
|
|
+ _assignments.AddRange(newAssignments);
|
|
|
|
|
|
AssignedEmployees.Refresh(false, true);
|
|
|
AvailableEmployees.Refresh(false, true);
|
|
|
@@ -1117,11 +1226,11 @@ public partial class JobResourcePlanner : UserControl
|
|
|
var dateIdx = _dates.IndexOf(date);
|
|
|
|
|
|
var emptimes = _assignments.Where(x =>
|
|
|
- jobIDs.Contains(x.JobID)
|
|
|
+ jobIDs.Contains(x.Job.ID)
|
|
|
&& (x.Date == date)
|
|
|
- && employees.Any(e => e.ID == x.EmployeeID)
|
|
|
+ && employees.Any(e => e.ID == x.Employee.ID)
|
|
|
).ToArray();
|
|
|
- var emptime = emptimes.Aggregate(TimeSpan.Zero, (time, ass) => time += ass.BookedDuration);
|
|
|
+ var emptime = emptimes.Aggregate(TimeSpan.Zero, (time, ass) => time += ass.Booked.Duration);
|
|
|
if(Properties.DisplayMode == JobPlannerDisplayMode.JobColumns)
|
|
|
{
|
|
|
System.Data.DataRow row = table.Rows[dateIdx];
|
|
|
@@ -1173,16 +1282,15 @@ public partial class JobResourcePlanner : UserControl
|
|
|
}
|
|
|
|
|
|
var assignments = _assignments.Where(x =>
|
|
|
- jobIDs.Contains(x.JobID)
|
|
|
+ jobIDs.Contains(x.Job.ID)
|
|
|
&& dates.Contains(x.Date)
|
|
|
- && employees.Any(e => e.ID == x.EmployeeID)
|
|
|
+ && employees.Any(e => e.ID == x.Employee.ID)
|
|
|
).ToArray();
|
|
|
- if (assignments.Any())
|
|
|
+ if (assignments.Length != 0)
|
|
|
{
|
|
|
using (new WaitCursor())
|
|
|
{
|
|
|
- var deletes = assignments.Select(x => new Assignment() { ID = x.ID }).ToArray();
|
|
|
- new Client<Assignment>().Delete(deletes, "Deleted from Job Planner");
|
|
|
+ Client.Delete(assignments, "Deleted from Job Planner");
|
|
|
|
|
|
var removes = AssignedEmployees.Items.Where(x => employees.Any(e => e.ID == x.ID)).ToArray();
|
|
|
foreach (var remove in removes)
|
|
|
@@ -1275,37 +1383,49 @@ public partial class JobResourcePlanner : UserControl
|
|
|
.Add(x=>x.SupervisionHours)
|
|
|
.Add(x=>x.Tradespersons)
|
|
|
.Add(x=>x.TradesHours);
|
|
|
-
|
|
|
- var stages = Client.Query<JobStage>(
|
|
|
- Filter<JobStage>.Where(x => x.StartDate).IsLessThanOrEqualTo(e.VisibleDateRange.ActualEndDate)
|
|
|
- .And(x => x.EndDate).IsGreaterThanOrEqualTo(e.VisibleDateRange.ActualStartDate),
|
|
|
- cols
|
|
|
- ).ToObjects<JobStage>()
|
|
|
- .ToArray();
|
|
|
-
|
|
|
- foreach (var stage in stages)
|
|
|
- CreateAppointment(stage);
|
|
|
+ var filter = Filter<JobStage>.Where(x => x.StartDate).IsLessThanOrEqualTo(e.VisibleDateRange.ActualEndDate)
|
|
|
+ .And(x => x.EndDate).IsGreaterThanOrEqualTo(e.VisibleDateRange.ActualStartDate);
|
|
|
+
|
|
|
+ var data = Client.QueryMultiple(
|
|
|
+ new KeyedQueryDef<JobStage>(filter, cols),
|
|
|
+ new KeyedQueryDef<Assignment>(
|
|
|
+ Filter<Assignment>.Where(x => x.JobStage.ID).InQuery(filter, x => x.ID),
|
|
|
+ Columns.None<Assignment>()
|
|
|
+ .Add(x => x.JobStage.ID)
|
|
|
+ .Add(x => x.Employee.Name)));
|
|
|
+ var stages = data.GetArray<JobStage>();
|
|
|
+ var assignments = data.GetObjects<Assignment>()
|
|
|
+ .GroupBy(x => x.JobStage.ID)
|
|
|
+ .ToDictionary(x => x.Key, x => x.Select(x => x.Employee.Name).Distinct().ToList());
|
|
|
+
|
|
|
+ foreach (var stage in stages)
|
|
|
+ CreateAppointment(stage, assignments.GetValueOrDefault(stage.ID));
|
|
|
Schedule.ItemsSource = appointments;
|
|
|
}
|
|
|
|
|
|
- private void CreateAppointment(JobStage stage)
|
|
|
+ private void CreateAppointment(JobStage stage, List<string>? employees)
|
|
|
{
|
|
|
- var model = new StageModel(stage);
|
|
|
+ var model = new StageModel(stage, employees ?? []);
|
|
|
appointments.Add(model);
|
|
|
}
|
|
|
|
|
|
private class StageModel : ScheduleAppointment
|
|
|
{
|
|
|
+ public List<string> Employees { get; }
|
|
|
+
|
|
|
+ public string ToolTipContent => $"Employees:\n{string.Join('\n', Employees.Select(x => $"- {x}"))}";
|
|
|
|
|
|
- public StageModel(JobStage stage)
|
|
|
+ public StageModel(JobStage stage, List<string> employees)
|
|
|
{
|
|
|
Stage = stage;
|
|
|
+ Employees = employees;
|
|
|
}
|
|
|
|
|
|
private JobStage _stage;
|
|
|
public JobStage Stage
|
|
|
{
|
|
|
get => _stage;
|
|
|
+ [MemberNotNull(nameof(_stage))]
|
|
|
set
|
|
|
{
|
|
|
_stage = value;
|
|
|
@@ -1360,7 +1480,7 @@ public partial class JobResourcePlanner : UserControl
|
|
|
stage.Type.CopyFrom(type);
|
|
|
if (new JobStagesGrid().EditItems([stage]))
|
|
|
{
|
|
|
- CreateAppointment(stage);
|
|
|
+ CreateAppointment(stage, null);
|
|
|
_copiedModel = null;
|
|
|
}
|
|
|
if (!types?.Any(x => x.ID == stage.Type.ID) == false)
|
|
|
@@ -1408,7 +1528,7 @@ public partial class JobResourcePlanner : UserControl
|
|
|
newstage.EndDate = newstage.StartDate + span;
|
|
|
|
|
|
new Client<JobStage>().Save(newstage,"Created from Project Planner");
|
|
|
- CreateAppointment(newstage);
|
|
|
+ CreateAppointment(newstage, null);
|
|
|
_copiedModel = null;
|
|
|
}
|
|
|
|