Skip to content

Commit 21f7990

Browse files
authored
Merge pull request #177 from VidetteMakes/tags
Part Traceability with Reusable Tags (limited rework support)
2 parents 2667022 + e1645f0 commit 21f7990

File tree

53 files changed

+5188
-1330
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+5188
-1330
lines changed

MESS/MESS.Blazor/Components/Pages/ProductionLog/ConfirmationModal.razor renamed to MESS/MESS.Blazor/Components/Dialogs/ConfirmationModal.razor

File renamed without changes.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<div class="toast-container position-fixed top-50 start-50 translate-middle p-3 @(IsVisible ? "d-block" : "d-none")"
2+
data-bs-autohide="false">
3+
4+
<div class="toast show confirmation-toast border-danger" role="alert" aria-live="assertive" aria-atomic="true">
5+
6+
<div class="toast-header">
7+
<strong class="me-auto">@HeaderText</strong>
8+
<button type="button" class="btn-close" aria-label="Close" @onclick="Close"></button>
9+
</div>
10+
11+
<div class="toast-body">
12+
<p class="mb-4">@BodyText</p>
13+
14+
<div class="d-flex justify-content-end">
15+
<button type="button" class="btn btn-danger" @onclick="Acknowledge">
16+
OK
17+
</button>
18+
</div>
19+
</div>
20+
21+
</div>
22+
</div>
23+
24+
@code {
25+
/// <summary>
26+
/// Determines whether the message modal is visible.
27+
/// </summary>
28+
[Parameter]
29+
public bool IsVisible { get; set; }
30+
31+
/// <summary>
32+
/// Event callback that is invoked when the visibility state changes.
33+
/// </summary>
34+
[Parameter]
35+
public EventCallback<bool> IsVisibleChanged { get; set; }
36+
37+
/// <summary>
38+
/// The text displayed in the modal header.
39+
/// </summary>
40+
[Parameter]
41+
public string? HeaderText { get; set; }
42+
43+
/// <summary>
44+
/// The text displayed in the modal body.
45+
/// </summary>
46+
[Parameter]
47+
public string? BodyText { get; set; }
48+
49+
/// <summary>
50+
/// Optional callback invoked when the user acknowledges the message.
51+
/// </summary>
52+
[Parameter]
53+
public EventCallback OnAcknowledge { get; set; }
54+
55+
/// <summary>
56+
/// Shows the message modal with the specified body and optional header text.
57+
/// </summary>
58+
/// <param name="bodyText">The text to display in the modal body.</param>
59+
/// <param name="headerText">Optional text to display in the modal header.</param>
60+
public void Show(string bodyText, string headerText = "")
61+
{
62+
HeaderText = headerText;
63+
BodyText = bodyText;
64+
IsVisible = true;
65+
StateHasChanged();
66+
}
67+
68+
/// <summary>
69+
/// Closes the message modal by hiding it and clearing its content.
70+
/// </summary>
71+
public void Close()
72+
{
73+
HeaderText = string.Empty;
74+
BodyText = string.Empty;
75+
IsVisible = false;
76+
StateHasChanged();
77+
}
78+
79+
private async Task Acknowledge()
80+
{
81+
Close();
82+
83+
if (OnAcknowledge.HasDelegate)
84+
{
85+
await OnAcknowledge.InvokeAsync();
86+
}
87+
}
88+
}

MESS/MESS.Blazor/Components/Dialogs/PartReworkDialog.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
@using MESS.Services.UI.ProductionLogEvent
33

44
@inject IProductionLogEventService ProductionLogEventService
5-
@inject IPartTraceabilityService PartTraceabilityService
65

76
@implements IDialogContentComponent
87

@@ -86,7 +85,8 @@
8685
.ToList();
8786

8887
// Load installed parts into memory using the service
89-
await PartTraceabilityService.LoadInstalledPartsIntoMemoryAsync(ids);
88+
//TODO
89+
//await PartTraceabilityService.LoadInstalledPartsIntoMemoryAsync(ids);
9090
9191
IsProcessing = false;
9292

MESS/MESS.Blazor/Components/Pages/Phoebe/PartManagement/PartDefinitionManager.razor

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@page "/parts"
22
@layout PhoebeLayout
33
@attribute [Authorize(Roles = "Technician, Administrator")]
4+
@using MESS.Blazor.Components.Dialogs
45
@using MESS.Blazor.Components.Layout
56
@using MESS.Data.Models
67
@using MESS.Services.CRUD.PartDefinitions
@@ -17,11 +18,38 @@
1718
}
1819
else
1920
{
20-
<div class="d-flex justify-content-between my-2" style="flex-direction: row">
21-
<h3>Part Definitions</h3>
22-
<div class="flex-grow-1"/>
23-
<input type="text" class="form-control w-auto" placeholder="Search..."
24-
@bind="SearchString" @bind:event="oninput" />
21+
<div class="d-flex flex-column flex-md-row align-items-center justify-content-between my-3 gap-2">
22+
<!-- Title -->
23+
<div class="d-flex align-items-center gap-2">
24+
<h3 class="mb-0">Part Definitions</h3>
25+
<ConfirmationModal @ref="_confirmationModal" OnSubmit="OnBatchMergeConfirmed" />
26+
</div>
27+
28+
<!-- Progress bar -->
29+
@if (_isMerging)
30+
{
31+
<div class="flex-grow-1 mx-2">
32+
<div class="progress" style="height: 25px;">
33+
<div class="progress-bar progress-bar-striped progress-bar-animated"
34+
role="progressbar"
35+
style="width:@_mergeProgress%"
36+
aria-valuenow="@_mergeProgress"
37+
aria-valuemin="0"
38+
aria-valuemax="100">
39+
@_mergeProgress% complete
40+
</div>
41+
</div>
42+
</div>
43+
}
44+
45+
<!-- Controls -->
46+
<div class="d-flex align-items-center gap-2">
47+
<button class="btn btn-primary" @onclick="ConfirmBatchMerge" title="Merge duplicate parts by name">
48+
Merge Duplicates
49+
</button>
50+
<input type="text" class="form-control w-auto" placeholder="Search..."
51+
@bind="SearchString" @bind:event="oninput" />
52+
</div>
2553
</div>
2654

2755
<MudTable @ref="_table"
@@ -38,7 +66,12 @@ else
3866

3967
<MudTh><MudTableSortLabel SortLabel="number_field" SortBy="new Func<PartDefinition, object>(x => x.Number ?? string.Empty)">
4068
<strong>Number</strong>
41-
</MudTableSortLabel></MudTh>
69+
</MudTableSortLabel></MudTh>
70+
71+
<MudTh>
72+
<strong>Has Unique Serial Numbers</strong>
73+
</MudTh>
74+
4275
<MudTh><strong>Actions</strong></MudTh>
4376
</HeaderContent>
4477

@@ -77,6 +110,11 @@ else
77110
_table.ReloadServerData();
78111
}
79112
}
113+
114+
private ConfirmationModal? _confirmationModal;
115+
116+
private bool _isMerging = false;
117+
private int _mergeProgress = 0;
80118

81119
protected override async Task OnInitializedAsync()
82120
{
@@ -140,6 +178,14 @@ else
140178

141179
private async Task HandleSubmitPartAsync(PartDefinition part)
142180
{
181+
// Validate first
182+
var validationMessage = ValidatePart(part);
183+
if (!string.IsNullOrEmpty(validationMessage))
184+
{
185+
ToastService.ShowWarning(validationMessage);
186+
return;
187+
}
188+
143189
try
144190
{
145191
PartDefinition? result;
@@ -169,7 +215,6 @@ else
169215
ToastService.ShowSuccess($"Updated part '{result.Name}'.");
170216
}
171217

172-
// Refresh data after a successful write
173218
await LoadParts();
174219
await _table.ReloadServerData();
175220
}
@@ -227,4 +272,63 @@ else
227272
$"Unexpected error deleting part '{part.Name}': {ex.Message}");
228273
}
229274
}
275+
276+
private string? ValidatePart(PartDefinition part)
277+
{
278+
if (string.IsNullOrWhiteSpace(part.Name))
279+
return "Name is required.";
280+
281+
bool duplicateExists = PartDefinitions
282+
.Where(p => p.Id != part.Id) // skip the part being edited
283+
.Any(p =>
284+
p.Name.Equals(part.Name, StringComparison.OrdinalIgnoreCase) &&
285+
(string.IsNullOrWhiteSpace(p.Number) && string.IsNullOrWhiteSpace(part.Number)
286+
|| p.Number == part.Number));
287+
288+
if (duplicateExists)
289+
return "A part with this name (and number) already exists.";
290+
291+
return null;
292+
}
293+
294+
private void ConfirmBatchMerge()
295+
{
296+
_confirmationModal?.Show(
297+
"This will merge all part definitions that share the same name. This operation cannot be undone. " +
298+
"Production data can be modified from this operation.",
299+
"Confirm Batch Merge");
300+
}
301+
302+
private async Task OnBatchMergeConfirmed(bool confirmed)
303+
{
304+
if (!confirmed) return;
305+
306+
try
307+
{
308+
_isMerging = true;
309+
_mergeProgress = 0;
310+
311+
var progress = new Progress<int>(percent =>
312+
{
313+
_mergeProgress = percent;
314+
StateHasChanged(); // re-render to update bar
315+
});
316+
317+
int mergedCount = await PartDefinitionService.BatchMergeByNameAsync(progress);
318+
319+
ToastService.ShowSuccess($"Batch merge completed. {mergedCount} merges performed.");
320+
321+
await LoadParts();
322+
await _table.ReloadServerData();
323+
}
324+
catch (Exception ex)
325+
{
326+
ToastService.ShowError($"Batch merge failed: {ex.Message}");
327+
}
328+
finally
329+
{
330+
_isMerging = false;
331+
_mergeProgress = 0;
332+
}
333+
}
230334
}

MESS/MESS.Blazor/Components/Pages/Phoebe/PartManagement/PartDefinitionTableRow.razor

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88
<MudTd DataLabel="Number">
99
<InputText id="number" @bind-Value="PartDefinition.Number" class="form-control" placeholder="Enter part number (optional)" @oninput="OnInputChanged"/>
1010
</MudTd>
11+
<MudTd DataLabel="Serial Number Unique">
12+
<InputCheckbox @bind-Value="PartDefinition.IsSerialNumberUnique" class="form-check-input" @onchange="OnInputChanged"/>
13+
</MudTd>
1114
<MudTd DataLabel="Actions">
1215
@if (!IsNewPart)
1316
{
1417
<div class="save-button-container">
15-
<button class="btn btn-sm btn-success" @onclick="HandleSubmit" disabled="@isSaving" title="Save">
18+
<button class="btn btn-sm btn-success" @onclick="HandleSubmit" disabled="@_isSaving" title="Save">
1619
<i class="bi bi-floppy"></i>
1720
@if (IsDirty)
1821
{
@@ -26,7 +29,7 @@
2629
}
2730
else
2831
{
29-
<button class="btn btn-sm btn-primary" @onclick="HandleSubmit" disabled="@isSaving">
32+
<button class="btn btn-sm btn-primary" @onclick="HandleSubmit" disabled="@_isSaving">
3033
<i class="bi bi-plus-lg"></i>
3134
</button>
3235
}
@@ -52,7 +55,7 @@
5255
public EventCallback<PartDefinition> OnSubmit { get; set; }
5356

5457
private bool IsNewPart => PartDefinition.Id == 0;
55-
private bool isSaving = false;
58+
private bool _isSaving = false;
5659
private bool IsDirty { get; set; } = false;
5760

5861
private void OnInputChanged(ChangeEventArgs e)
@@ -65,9 +68,9 @@
6568
if (!IsDirty)
6669
return;
6770

68-
isSaving = true;
71+
_isSaving = true;
6972
await OnSubmit.InvokeAsync(PartDefinition);
70-
isSaving = false;
73+
_isSaving = false;
7174
IsDirty = false;
7275
}
7376

MESS/MESS.Blazor/Components/Pages/Phoebe/WorkInstruction/WorkInstructionNodes/PartNodeView.razor

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,33 @@
3636
type="checkbox"
3737
role="switch"
3838
id="inputTypeSwitch_@PartNode!.ClientId"
39-
checked="@(PartNode.InputType == PartInputType.ProductionLogId)"
39+
checked="@(PartNode.InputType == PartInputType.Tag)"
4040
@onclick="ToggleInputType" />
4141

4242
<label class="form-check-label toggle-label"
4343
for="inputTypeSwitch_@PartNode.ClientId">
4444
@(PartNode.InputType == PartInputType.SerialNumber
4545
? "Serial"
46-
: "Production Log")
46+
: "Tag")
47+
</label>
48+
</div>
49+
</div>
50+
51+
<!-- Serial Number Uniqueness Toggle -->
52+
<div class="d-flex flex-column align-items-start ms-3 me-3">
53+
<label class="form-label mb-1">Serial Uniqueness</label>
54+
<div class="form-check form-switch">
55+
<input class="form-check-input toggle-switch"
56+
type="checkbox"
57+
role="switch"
58+
id="serialUniqueSwitch_@PartNode!.ClientId"
59+
checked="@(PartNode.IsSerialNumberUnique)"
60+
disabled="@(PartNode.InputType != PartInputType.SerialNumber)"
61+
@onclick="ToggleSerialUniqueness" />
62+
63+
<label class="form-check-label toggle-label"
64+
for="serialUniqueSwitch_@PartNode.ClientId">
65+
@(PartNode.IsSerialNumberUnique ? "Unique" : "Non-Unique")
4766
</label>
4867
</div>
4968
</div>
@@ -56,7 +75,6 @@
5675
</div>
5776

5877
@code {
59-
6078
[Parameter]
6179
public EventCallback<(WorkInstructionNodeFormDTO node, string action)> OnNodeAction { get; set; }
6280

@@ -89,9 +107,19 @@
89107
return;
90108

91109
PartNode.InputType = PartNode.InputType == PartInputType.SerialNumber
92-
? PartInputType.ProductionLogId
110+
? PartInputType.Tag
93111
: PartInputType.SerialNumber;
94112

95113
await NotifyChangedAsync();
96114
}
115+
116+
private async Task ToggleSerialUniqueness()
117+
{
118+
if (PartNode is null)
119+
return;
120+
121+
PartNode.IsSerialNumberUnique = !PartNode.IsSerialNumberUnique;
122+
123+
await NotifyChangedAsync();
124+
}
97125
}

0 commit comments

Comments
 (0)