Erstes Formular + QR Integration

This commit is contained in:
Marc Wieland 2025-11-04 23:29:21 +01:00
parent b0edcb2af1
commit 697367dd54
12 changed files with 266 additions and 21 deletions

View File

@ -4,14 +4,13 @@
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
<p>Seite nicht gefunden.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
</CascadingAuthenticationState>

View File

@ -0,0 +1,9 @@
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />
@code {
[Parameter]
public string? Action { get; set; }
}

View File

@ -0,0 +1,91 @@
@page "/filterform"
@inject IJSRuntime JS
@using FilterCair.Shared.Models
<PageTitle>Filterdaten erfassen</PageTitle>
<div class="container py-4">
<h4 class="mb-4 text-center">Filterdaten erfassen</h4>
<EditForm Model="@filter" OnValidSubmit="SaveForm">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="mb-3">
<label class="form-label">Filter-ID</label>
<InputText class="form-control" @bind-Value="filter.FilterId" placeholder="z. B. FC-A-234" />
</div>
<div class="mb-3">
<label class="form-label">Halle</label>
<InputSelect class="form-select" @bind-Value="filter.Halle">
<option value=""> bitte wählen </option>
@foreach (var h in halls)
{
<option value="@h">@h</option>
}
</InputSelect>
</div>
<div class="mb-3">
<label class="form-label">Zustand</label>
<InputSelect class="form-select" @bind-Value="filter.Zustand">
<option value=""> bitte wählen </option>
<option>In Ordnung</option>
<option>Verschmutzt</option>
<option>Defekt</option>
</InputSelect>
</div>
<div class="mb-3">
<label class="form-label">Luftdruck (Pa)</label>
<InputNumber class="form-control" @bind-Value="filter.Luftdruck" />
</div>
<div class="mb-3">
<label class="form-label">Bemerkung</label>
<InputTextArea class="form-control" rows="3" @bind-Value="filter.Bemerkung" />
</div>
<button type="submit" class="btn btn-success w-100">
Speichern
</button>
</EditForm>
@if (saved)
{
<div class="alert alert-success mt-4 text-center">
Daten gespeichert!
</div>
}
</div>
@code {
private FilterModel filter = new();
private bool saved = false;
private List<string> halls = new() { "Halle A", "Halle B", "Halle C" };
private async Task SaveForm()
{
saved = true;
Console.WriteLine($"Gespeichert: {System.Text.Json.JsonSerializer.Serialize(filter)}");
// später → JS.InvokeVoidAsync("IndexedDB.saveFilter", filter);
await JS.InvokeVoidAsync("console.log", "Filter gespeichert:", filter);
}
// optional: QR-Ergebnis vorbelegen
protected override void OnInitialized()
{
var qrParam = NavManager?.ToAbsoluteUri(NavManager.Uri).Query;
if (!string.IsNullOrEmpty(qrParam))
{
// z. B. ?id=FC-123
var parts = System.Web.HttpUtility.ParseQueryString(qrParam);
filter.FilterId = parts.Get("id");
}
}
[Inject] NavigationManager? NavManager { get; set; }
}

View File

@ -110,7 +110,8 @@
private void Login()
{
Nav.NavigateTo("authentication/login");
Nav.NavigateTo("authentication/login", forceLoad: true);
}
private void Logout()

View File

@ -0,0 +1,53 @@
@page "/qrscanner"
@inject IJSRuntime JS
<PageTitle>QR-Scanner</PageTitle>
<div class="container py-4 text-center">
<h4 class="mb-3">📷 QR-Code Scanner</h4>
<div class="mb-3">
<video id="video" autoplay playsinline class="border rounded shadow-sm" style="width:100%;max-width:400px;"></video>
</div>
@if (!string.IsNullOrEmpty(result))
{
<div class="alert alert-success mt-3">
<strong>Erkannt:</strong> @result
</div>
}
<div class="mt-4">
<button class="btn btn-secondary me-2" @onclick="StartScan">Neu starten</button>
<button class="btn btn-outline-danger" @onclick="StopScan">Stop</button>
</div>
</div>
@code {
private string? result;
private async Task StartScan()
{
await JS.InvokeVoidAsync("QRScanner.start");
}
private async Task StopScan()
{
await JS.InvokeVoidAsync("QRScanner.stop");
}
[JSInvokable]
public void OnQrDetected(string code)
{
result = code;
StateHasChanged();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("QRScanner.init", DotNetObjectReference.Create(this));
}
}
}

View File

@ -7,13 +7,12 @@ var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
var apiBase = builder.Configuration["Api:BaseUrl"];
builder.Services.AddHttpClient("FilterCair.ServerAPI", client =>
client.BaseAddress = new Uri(apiBase))
client.BaseAddress = new Uri(builder.Configuration["Api:BaseUrl"] ?? "https://localhost:7010"))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiBase) });
builder.Services.AddScoped(sp =>
sp.GetRequiredService<IHttpClientFactory>().CreateClient("FilterCair.ServerAPI"));
builder.Services.AddMsalAuthentication(options =>
{
@ -22,4 +21,4 @@ builder.Services.AddMsalAuthentication(options =>
"api://54b010c7-4a9a-4f2d-bea3-9faba3f12495/API.Access");
});
await builder.Build().RunAsync();
await builder.Build().RunAsync();

View File

@ -1,12 +1,19 @@
{
"ApiUrl": "https://filtercair-api.azurewebsites.net",
"AzureAd": {
"Authority": "https://login.microsoftonline.com/66c8d79d-dbbb-46ca-9b6d-3b942f463abe",
"ClientId": "54b010c7-4a9a-4f2d-bea3-9faba3f12495",
"ValidateAuthority": true
"ValidateAuthority": true,
"RedirectUri": "https://filtercair-client-efava4bfgvamhkfu.westeurope-01.azurewebsites.net/authentication/login-callback",
"PostLogoutRedirectUri": "https://filtercair-client-efava4bfgvamhkfu.westeurope-01.azurewebsites.net/",
"CacheLocation": "localStorage",
"Scopes": [
"openid",
"profile",
"email",
"api://54b010c7-4a9a-4f2d-bea3-9faba3f12495/API.Access"
]
},
"Api": {
"Scopes": [ "api://54b010c7-4a9a-4f2d-bea3-9faba3f12495/API.Access" ],
"BaseUrl": "https://localhost:7010"
"BaseUrl": "https://filtercair-api.azurewebsites.net"
}
}

View File

@ -43,6 +43,7 @@
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js"></script>
<script src="js/filtercair.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>

View File

@ -19,4 +19,70 @@
);
});
}
};
};
window.QRScanner = {
video: null,
stream: null,
dotnetRef: null,
scanning: false,
init: function (dotnetRef) {
this.dotnetRef = dotnetRef;
console.log("QRScanner initialized");
},
start: async function () {
if (!this.video) {
this.video = document.getElementById("video");
}
try {
this.stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } });
this.video.srcObject = this.stream;
this.scanning = true;
this.scanLoop();
} catch (err) {
alert("Kamera konnte nicht gestartet werden: " + err);
}
},
stop: function () {
if (this.stream) {
this.stream.getTracks().forEach(t => t.stop());
}
this.scanning = false;
},
scanLoop: async function () {
if (!this.video) return;
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const jsQRAvailable = typeof jsQR !== 'undefined';
if (!jsQRAvailable) {
console.warn("jsQR not loaded yet.");
return;
}
const tick = () => {
if (!this.scanning) return;
if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) {
canvas.width = this.video.videoWidth;
canvas.height = this.video.videoHeight;
context.drawImage(this.video, 0, 0, canvas.width, canvas.height);
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height);
if (code) {
console.log("QR erkannt:", code.data);
this.dotnetRef.invokeMethodAsync("OnQrDetected", code.data);
this.stop();
return;
}
}
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}
};

View File

@ -1,6 +0,0 @@
namespace FilterCair.Shared;
public class Class1
{
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FilterCair.Shared.Models
{
public class FilterModel
{
[Required]
public string? FilterId { get; set; }
[Required]
public string? Halle { get; set; }
public string? Zustand { get; set; }
public double? Luftdruck { get; set; }
public string? Bemerkung { get; set; }
}
}

View File

@ -59,4 +59,7 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {57097AF9-FD61-420B-B0E9-520CF180D7FC}
EndGlobalSection
EndGlobal