feat: Add CutList.Web Blazor Server application
Add a new web-based frontend for cut list optimization using: - Blazor Server with .NET 8 - Entity Framework Core with MSSQL LocalDB - Full CRUD for Materials, Suppliers, Projects, and Cutting Tools - Supplier stock length management for quick project setup - Integration with CutList.Core for bin packing optimization - Print-friendly HTML reports with efficiency statistics Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
66
CutList.Web/Components/Shared/ConfirmDialog.razor
Normal file
66
CutList.Web/Components/Shared/ConfirmDialog.razor
Normal file
@@ -0,0 +1,66 @@
|
||||
@if (IsVisible)
|
||||
{
|
||||
<div class="modal fade show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@Title</h5>
|
||||
<button type="button" class="btn-close" @onclick="Cancel"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>@Message</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @onclick="Cancel">Cancel</button>
|
||||
<button type="button" class="btn @ConfirmButtonClass" @onclick="Confirm">@ConfirmText</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string Title { get; set; } = "Confirm";
|
||||
|
||||
[Parameter]
|
||||
public string Message { get; set; } = "Are you sure?";
|
||||
|
||||
[Parameter]
|
||||
public string ConfirmText { get; set; } = "Confirm";
|
||||
|
||||
[Parameter]
|
||||
public string ConfirmButtonClass { get; set; } = "btn-danger";
|
||||
|
||||
[Parameter]
|
||||
public EventCallback OnConfirm { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback OnCancel { get; set; }
|
||||
|
||||
public bool IsVisible { get; private set; }
|
||||
|
||||
public void Show()
|
||||
{
|
||||
IsVisible = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
IsVisible = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task Confirm()
|
||||
{
|
||||
Hide();
|
||||
await OnConfirm.InvokeAsync();
|
||||
}
|
||||
|
||||
private async Task Cancel()
|
||||
{
|
||||
Hide();
|
||||
await OnCancel.InvokeAsync();
|
||||
}
|
||||
}
|
||||
88
CutList.Web/Components/Shared/CutListReport.razor
Normal file
88
CutList.Web/Components/Shared/CutListReport.razor
Normal file
@@ -0,0 +1,88 @@
|
||||
@using CutList.Core
|
||||
@using CutList.Core.Nesting
|
||||
@using CutList.Core.Formatting
|
||||
@inject ReportService ReportService
|
||||
|
||||
<div class="cut-list-report">
|
||||
<header class="report-header">
|
||||
<h1>CUT LIST</h1>
|
||||
<div class="meta-info">
|
||||
<div class="meta-row"><span>Date:</span> @DateTime.Now.ToString("g")</div>
|
||||
<div class="meta-row"><span>Project:</span> @Project.Name</div>
|
||||
@if (Project.Material != null)
|
||||
{
|
||||
<div class="meta-row"><span>Material:</span> @Project.Material.Shape - @Project.Material.Size</div>
|
||||
}
|
||||
@if (Project.CuttingTool != null)
|
||||
{
|
||||
<div class="meta-row"><span>Cut Method:</span> @Project.CuttingTool.Name (kerf: @Project.CuttingTool.KerfInches")</div>
|
||||
}
|
||||
<div class="meta-row"><span>Stock Bars:</span> @PackResult.Bins.Count</div>
|
||||
<div class="meta-row"><span>Total Pieces:</span> @TotalPieces</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@foreach (var (bin, index) in PackResult.Bins.Select((b, i) => (b, i + 1)))
|
||||
{
|
||||
<section class="bin-section">
|
||||
<h2>BAR #@index - Length: @ReportService.FormatLength(bin.Length)</h2>
|
||||
<table class="cuts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 60px;">Qty</th>
|
||||
<th style="width: 120px;">Length</th>
|
||||
<th>Label</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var group in ReportService.GroupItems(bin.Items))
|
||||
{
|
||||
<tr>
|
||||
<td>@group.Count</td>
|
||||
<td>@ReportService.FormatLength(group.Length)</td>
|
||||
<td>@group.Name</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="drop">
|
||||
Remaining drop: @ReportService.FormatLength(bin.RemainingLength)
|
||||
(@((bin.Utilization * 100).ToString("F1"))% utilization)
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<footer class="summary">
|
||||
<h2>SUMMARY</h2>
|
||||
<div class="summary-grid">
|
||||
<div class="summary-row"><span>Stock Bars Needed:</span> <strong>@PackResult.Bins.Count</strong></div>
|
||||
<div class="summary-row"><span>Total Pieces:</span> <strong>@TotalPieces</strong></div>
|
||||
<div class="summary-row"><span>Total Material:</span> <strong>@ReportService.FormatLength(TotalMaterial)</strong></div>
|
||||
<div class="summary-row"><span>Total Used:</span> <strong>@ReportService.FormatLength(TotalUsed)</strong></div>
|
||||
<div class="summary-row"><span>Total Waste:</span> <strong>@ReportService.FormatLength(TotalWaste)</strong></div>
|
||||
<div class="summary-row"><span>Efficiency:</span> <strong>@Efficiency.ToString("F1")%</strong></div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Project.Notes))
|
||||
{
|
||||
<div class="notes-section">
|
||||
<h3>Notes</h3>
|
||||
<p>@Project.Notes</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public Project Project { get; set; } = null!;
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public PackResult PackResult { get; set; } = null!;
|
||||
|
||||
private int TotalPieces => PackResult.Bins.Sum(b => b.Items.Count);
|
||||
private double TotalMaterial => PackResult.Bins.Sum(b => b.Length);
|
||||
private double TotalUsed => PackResult.Bins.Sum(b => b.UsedLength);
|
||||
private double TotalWaste => PackResult.Bins.Sum(b => b.RemainingLength);
|
||||
private double Efficiency => TotalMaterial > 0 ? TotalUsed / TotalMaterial * 100 : 0;
|
||||
}
|
||||
75
CutList.Web/Components/Shared/LengthInput.razor
Normal file
75
CutList.Web/Components/Shared/LengthInput.razor
Normal file
@@ -0,0 +1,75 @@
|
||||
@using CutList.Core.Formatting
|
||||
|
||||
<div class="length-input">
|
||||
<input type="text"
|
||||
class="form-control @(HasError ? "is-invalid" : "")"
|
||||
value="@DisplayValue"
|
||||
@onchange="OnInputChange"
|
||||
placeholder="e.g., 12' 6" or 144" />
|
||||
@if (HasError)
|
||||
{
|
||||
<div class="invalid-feedback">@ErrorMessage</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public decimal Value { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<decimal> ValueChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? Placeholder { get; set; }
|
||||
|
||||
private string DisplayValue { get; set; } = string.Empty;
|
||||
private bool HasError { get; set; }
|
||||
private string ErrorMessage { get; set; } = string.Empty;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (Value > 0 && string.IsNullOrEmpty(DisplayValue))
|
||||
{
|
||||
DisplayValue = ArchUnits.FormatFromInches((double)Value);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnInputChange(ChangeEventArgs e)
|
||||
{
|
||||
var input = e.Value?.ToString() ?? string.Empty;
|
||||
DisplayValue = input;
|
||||
HasError = false;
|
||||
ErrorMessage = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
await ValueChanged.InvokeAsync(0);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Try to parse as architectural units
|
||||
var inches = ArchUnits.ParseToInches(input);
|
||||
await ValueChanged.InvokeAsync((decimal)inches);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Try to parse as plain decimal (inches)
|
||||
if (decimal.TryParse(input, out var decimalValue))
|
||||
{
|
||||
await ValueChanged.InvokeAsync(decimalValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
HasError = true;
|
||||
ErrorMessage = "Invalid format. Use feet (12'), inches (6\"), or decimal (144)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string FormatLength(decimal inches)
|
||||
{
|
||||
return ArchUnits.FormatFromInches((double)inches);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user