Skip to content

Commit cbb6d15

Browse files
authored
Implement task steps (#17)
* Set data design for this Feature * Added TaskDefinitionStep, TaskInstanceStep * Allow grabbing Steps from parents * Show steps on home page * Check off steps from Home * Make OnlyActive default * Add modal to add steps to Task Definition edit page
1 parent f5758ac commit cbb6d15

32 files changed

+2569
-22
lines changed

.devcontainer/devcontainer.json

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"GitHub.copilot-chat",
4040
"github.vscode-github-actions",
4141
"ms-dotnettools.csharp",
42+
"ms-dotnettools.csdevkit",
4243
"ms-mssql.mssql"
4344
]
4445
}

.docs/DATA-DESIGN.md

+15-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
* **`TaskType`**
3535
* **`Asset`(s)**
3636
* Task name
37-
* Task system-name (kebab-name but overridable)
3837
* (Phase1) `TaskInstance`
3938
* An instance of a **`TaskDefinition`**.
4039
* Can be auto-generated when a required lifespan hits.
@@ -49,6 +48,21 @@
4948
* Duration (Self-Recorded)
5049
* DueByDate
5150
* MQTT-enabled (can be _Phase4_)
51+
* (Phase2) `TaskDefinitionSteps`
52+
* _ie, for 'Clean Room', steps may be 'Organize books', 'Clear dresser', 'Hang clothes', etc. ..._
53+
* `TaskDefinitionStep`s are associated under **`TaskDefinition`s**
54+
* Each `TaskInstance` will have its own set of `TaskDefinitionStep`s to track progress.
55+
* Properties:
56+
* **`TaskDefinition`**
57+
* Task Definition Step name
58+
* IsOptional
59+
* (Phase2) `TaskInstanceStep`
60+
* `TaskInstanceStep`s are associated under **`TaskInstance`s** and created from **`TaskDefinitionStep`s**
61+
* Properties:
62+
* **`TaskInstance`**
63+
* **`TaskDefinitionStep`**
64+
* CompletedOn
65+
* CompletedBy (User)
5266
* (Phase2) `Part`
5367
* Parts contain `PurchasedPart`s
5468
* Properties:

src/MaintenanceLog.Client/Components/TaskInstances/TaskInstanceGrid.razor

+78-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
@using MaintenanceLog.Data.Entities
22
@using MaintenanceLog.Data.Services.Contracts
33
@using MaintenanceLog.Common.Extensions
4+
@using System.Security.Claims
45

56
@inject IJSRuntime JsRuntime
67
@inject ITaskDefinitionService TaskDefinitionService
78
@inject ITaskInstanceService TaskInstanceService
9+
@inject ITaskInstanceStepService TaskInstanceStepService
10+
@inject AuthenticationStateProvider AuthenticationStateProvider
811
@inject NavigationManager NavigationManager
912

1013
<!-- Task Instances Table -->
@@ -13,8 +16,9 @@
1316
<table class="table">
1417
<thead>
1518
<tr>
16-
<th>Start</th>
19+
<th></th>
1720
<th>Name</th>
21+
<th>Steps</th>
1822
<th>Asset</th>
1923
<th>Area</th>
2024
<th>Completed On</th>
@@ -29,14 +33,47 @@
2933
<td>
3034
@if (taskInstance.CompletedOn is null)
3135
{
32-
<button class="btn btn-danger" title="Stop" @onclick="() => StopTaskInstance(taskInstance)">
33-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="white" class="bi bi-stop" viewBox="0 0 16 16">
34-
<path d="M3.5 5A1.5 1.5 0 0 1 5 3.5h6A1.5 1.5 0 0 1 12.5 5v6a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 11zM5 4.5a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5V5a.5.5 0 0 0-.5-.5z"/>
35-
</svg>
36-
</button>
36+
<button class="btn btn-danger" title="Stop" @onclick="() => StopTaskInstance(taskInstance)">
37+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="white" class="bi bi-stop" viewBox="0 0 16 16">
38+
<path d="M3.5 5A1.5 1.5 0 0 1 5 3.5h6A1.5 1.5 0 0 1 12.5 5v6a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 11zM5 4.5a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5V5a.5.5 0 0 0-.5-.5z" />
39+
</svg>
40+
</button>
3741
}
3842
</td>
3943
<td>@taskInstance.TaskDefinition!.Name</td>
44+
<td>
45+
@if (@taskInstance.TaskInstanceSteps?.Count > 0)
46+
{
47+
var completedSteps = taskInstance.TaskInstanceSteps.Where(s => s.CompletedOn.HasValue).Count();
48+
if (completedSteps == taskInstance.TaskInstanceSteps.Count || taskInstance.CompletedOn.HasValue)
49+
{
50+
<span class="badge bg-success">@completedSteps / @taskInstance.TaskInstanceSteps.Count</span>
51+
}
52+
else
53+
{
54+
<span class="badge bg-info">@completedSteps / @taskInstance.TaskInstanceSteps.Count</span>
55+
}
56+
<ul>
57+
@foreach (var step in taskInstance.TaskInstanceSteps)
58+
{
59+
<li>
60+
@if (step.CompletedOn.HasValue)
61+
{
62+
<s>
63+
<input type="checkbox" @onchange="() => HandleInstanceStepCheckboxChanged(step)" checked="@step.CompletedOn.HasValue" />
64+
@step.TaskDefinitionStep!.Name
65+
</s>
66+
}
67+
else
68+
{
69+
<input type="checkbox" @onchange="() => HandleInstanceStepCheckboxChanged(step)" checked="@step.CompletedOn.HasValue" />
70+
@step.TaskDefinitionStep!.Name
71+
}
72+
</li>
73+
}
74+
</ul>
75+
}
76+
</td>
4077
<td>@taskInstance.TaskDefinition!.Asset?.Name</td>
4178
<td>@taskInstance.TaskDefinition!.Area?.Name</td>
4279
<td>
@@ -76,11 +113,13 @@ else
76113
public int? TaskDefinitionId { get; set; }
77114
[Parameter]
78115
public bool? OnlyShowActive { get; set; }
116+
private string? _userId;
79117

80118
private List<TaskInstance>? taskInstances;
81119

82120
protected override async Task OnInitializedAsync()
83121
{
122+
await GetClaimsPrincipalData();
84123
await base.OnInitializedAsync();
85124
}
86125

@@ -90,13 +129,20 @@ else
90129
await LoadTaskInstances();
91130
}
92131

132+
private async Task GetClaimsPrincipalData()
133+
{
134+
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
135+
var user = authState.User;
136+
_userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
137+
}
138+
93139
private async Task LoadTaskInstances()
94140
{
95141
taskInstances = (await TaskInstanceService.GetAsync())
96142
.Where(t => !TaskDefinitionId.HasValue || (t.TaskDefinitionId == TaskDefinitionId))
97143
.Where(t => !OnlyShowActive.HasValue || OnlyShowActive == false || (OnlyShowActive == true && t.CompletedOn == null))
98144
.ToList();
99-
145+
100146
await InvokeAsync(StateHasChanged);
101147
}
102148

@@ -119,4 +165,29 @@ else
119165
await TaskInstanceService.UpdateAsync(taskInstance);
120166
await LoadTaskInstances();
121167
}
168+
169+
private async Task HandleInstanceStepCheckboxChanged(TaskInstanceStep taskInstanceStep)
170+
{
171+
if (taskInstanceStep.CompletedOn.HasValue)
172+
{
173+
await UnCheckInstanceStep(taskInstanceStep);
174+
}
175+
else
176+
{
177+
await CheckOffInstanceStep(taskInstanceStep);
178+
}
179+
await LoadTaskInstances();
180+
}
181+
182+
private async Task CheckOffInstanceStep(TaskInstanceStep taskInstanceStep)
183+
{
184+
taskInstanceStep.CompletedOn = DateTimeOffset.UtcNow;
185+
await TaskInstanceStepService.UpdateAsync(taskInstanceStep);
186+
}
187+
188+
private async Task UnCheckInstanceStep(TaskInstanceStep taskInstanceStep)
189+
{
190+
taskInstanceStep.CompletedOn = null!;
191+
await TaskInstanceStepService.UpdateAsync(taskInstanceStep);
192+
}
122193
}

src/MaintenanceLog.Client/Pages/Home.razor

+6-9
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,19 @@
1616
Hello @context.User.Identity?.Name!
1717

1818
<h2 class="pt-4">Tasks</h2>
19-
<input type="checkbox" @bind-value="onlyActive" @bind-value:after="OnOnlyActiveChanged" /> Only Active
20-
<TaskInstanceGrid OnlyShowActive="onlyActive" />
19+
<input type="checkbox" @bind="onlyActive" /> Only Active
20+
21+
<TaskInstanceGrid OnlyShowActive="@onlyActive" />
2122
</Authorized>
2223
</AuthorizeView>
2324

2425
@code
2526
{
26-
private bool? onlyActive = false;
27+
private bool? onlyActive = true;
2728

2829
protected override async Task OnAfterRenderAsync(bool firstRender)
2930
{
30-
//onlyActive = await LocalStorage.GetItemAsync<bool?>("onlyActive");
31-
}
32-
33-
private async Task OnOnlyActiveChanged()
34-
{
35-
//await LocalStorage.SetItemAsync("onlyActive", onlyActive);
31+
//onlyActive = await LocalStorage.GetItemAsync<bool?>(nameof(onlyActive));
32+
await base.OnAfterRenderAsync(firstRender);
3633
}
3734
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
@using MaintenanceLog.Data.Entities
2+
@using MaintenanceLog.Data.Services.Contracts
3+
4+
@inject ITaskDefinitionStepService TaskDefinitionStepService
5+
6+
<button class="btn btn-primary" @onclick="OpenModal">
7+
+ Step
8+
</button>
9+
10+
@if (isModalOpen && taskDefinitionStep is not null)
11+
{
12+
<div class="modal">
13+
<div class="modal-content">
14+
<span class="close" @onclick="CloseModal">&times;</span>
15+
<p>Add a new Task Definition Step:</p>
16+
17+
<input type="text" class="form-control" placeholder="Name" @bind="taskDefinitionStep.Name" />
18+
<div class="form-check">
19+
<input class="form-check-input" type="checkbox" id="isOptionalCheck" @bind="taskDefinitionStep.IsOptional" />
20+
<label class="form-check-label" for="isOptionalCheck">Is Optional</label>
21+
</div>
22+
23+
<button class="btn btn-primary" @onclick="AddTaskDefinitionStep">
24+
Add
25+
</button>
26+
</div>
27+
</div>
28+
}
29+
30+
<style>
31+
32+
.modal {
33+
display: block; /* Hidden by default */
34+
position: fixed; /* Stay in place */
35+
z-index: 1; /* Sit on top */
36+
left: 0;
37+
top: 0;
38+
width: 100%; /* Full width */
39+
height: 100%; /* Full height */
40+
overflow: auto; /* Enable scroll if needed */
41+
background-color: rgb(0,0,0); /* Fallback color */
42+
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
43+
}
44+
45+
.modal-content {
46+
background-color: #fefefe;
47+
margin: 15% auto; /* 15% from the top and centered */
48+
padding: 20px;
49+
border: 1px solid #888;
50+
width: 80%; /* Could be more or less, depending on screen size */
51+
}
52+
53+
.close {
54+
color: #aaa;
55+
float: right;
56+
font-size: 28px;
57+
font-weight: bold;
58+
}
59+
60+
.close:hover,
61+
.close:focus {
62+
color: black;
63+
text-decoration: none;
64+
cursor: pointer;
65+
}
66+
67+
</style>
68+
69+
@code {
70+
[Parameter]
71+
public int TaskDefinitionId { get; set; }
72+
73+
private bool isModalOpen = false;
74+
private TaskDefinitionStep? taskDefinitionStep;
75+
76+
void OpenModal()
77+
{
78+
taskDefinitionStep = new TaskDefinitionStep()
79+
{
80+
Name = "",
81+
TaskDefinitionId = TaskDefinitionId
82+
};
83+
isModalOpen = true;
84+
}
85+
86+
void CloseModal()
87+
{
88+
isModalOpen = false;
89+
}
90+
91+
private async Task AddTaskDefinitionStep()
92+
{
93+
if (taskDefinitionStep is null)
94+
{
95+
return;
96+
}
97+
await TaskDefinitionStepService.AddAsync(taskDefinitionStep);
98+
isModalOpen = false;
99+
}
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
@using MaintenanceLog.Data.Entities
2+
@using MaintenanceLog.Data.Services.Contracts
3+
4+
@inject IJSRuntime JsRuntime
5+
@inject ITaskDefinitionStepService TaskDefinitionStepService
6+
7+
<!-- Task Definition Steps Table -->
8+
@if (taskDefinitionSteps != null)
9+
{
10+
<table class="table">
11+
<thead>
12+
<tr>
13+
<th>Name</th>
14+
<th>Is Optional</th>
15+
<th>Actions</th>
16+
</tr>
17+
</thead>
18+
<tbody>
19+
@foreach (var step in taskDefinitionSteps)
20+
{
21+
<tr>
22+
<td>@step.Name</td>
23+
<td>@step.IsOptional</td>
24+
<td>
25+
<button class="btn btn-danger" @onclick="() => DeleteTaskDefinitionStep(step.Id)">Delete</button>
26+
</td>
27+
</tr>
28+
}
29+
</tbody>
30+
</table>
31+
32+
<AddTaskDefinitionStepModal TaskDefinitionId="TaskDefinitionId" />
33+
}
34+
35+
36+
@code {
37+
[Parameter]
38+
public int TaskDefinitionId { get; set; }
39+
40+
private List<TaskDefinitionStep>? taskDefinitionSteps;
41+
42+
protected override async Task OnInitializedAsync()
43+
{
44+
await base.OnInitializedAsync();
45+
await LoadTaskDefinitionSteps();
46+
}
47+
48+
private async Task LoadTaskDefinitionSteps()
49+
{
50+
taskDefinitionSteps = (await TaskDefinitionStepService.GetAsync())
51+
.Where(t => t.TaskDefinitionId == TaskDefinitionId)
52+
.ToList();
53+
54+
await InvokeAsync(StateHasChanged);
55+
}
56+
57+
private async Task DeleteTaskDefinitionStep(int id)
58+
{
59+
bool confirmed = await JsRuntime.InvokeAsync<bool>("confirm", "Are you sure?");
60+
if (confirmed)
61+
{
62+
await TaskDefinitionStepService.DeleteAsync(id);
63+
await LoadTaskDefinitionSteps();
64+
}
65+
}
66+
}

src/MaintenanceLog.Client/Pages/TaskDefinitions/EditTaskDefinition.razor

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@page "/task-definitions/edit/{TaskDefinitionId:int}"
22

3+
@using MaintenanceLog.Client.Pages.TaskDefinitionSteps
34
@using MaintenanceLog.Data.Entities
45
@using MaintenanceLog.Data.Services.Contracts
56
@using Microsoft.AspNetCore.Authorization
@@ -67,6 +68,8 @@
6768
<!-- Submit Button -->
6869
<button type="submit" class="btn btn-primary">Save</button>
6970
</EditForm>
71+
72+
<TaskDefinitionStepGrid TaskDefinitionId="TaskDefinitionId" />
7073
}
7174
else
7275
{

0 commit comments

Comments
 (0)