Neuste Version

This commit is contained in:
Marc Wieland 2025-11-05 17:37:44 +01:00
parent c4ad142e4f
commit 0bf240ccda
13 changed files with 541 additions and 89 deletions

View File

@ -40,7 +40,7 @@
<th>Ansprechpartner</th>
<th>Telefon</th>
<th>Email</th>
<th style="width:120px;"></th>
<th style="width:160px;"></th>
</tr>
</thead>
<tbody>
@ -53,6 +53,10 @@
<td>@c.Telefon</td>
<td>@c.Email</td>
<td class="text-end">
<button class="btn btn-outline-primary btn-sm rounded-pill me-1"
@onclick="() => ShowEditModal(c)">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-outline-danger btn-sm rounded-pill"
@onclick="() => DeleteCustomer(c)">
<i class="bi bi-trash"></i>
@ -72,7 +76,10 @@
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title"><i class="bi bi-person-plus me-2"></i> Neuer Kunde</h5>
<h5 class="modal-title">
<i class="bi @(isEditMode ? "bi-pencil" : "bi-person-plus") me-2"></i>
@(isEditMode ? "Kunde bearbeiten" : "Neuer Kunde")
</h5>
<button type="button" class="btn-close btn-close-white" @onclick="CloseModal"></button>
</div>
<div class="modal-body">
@ -103,7 +110,9 @@
<div class="text-end">
<button type="button" class="btn btn-outline-secondary me-2 rounded-pill" @onclick="CloseModal">Abbrechen</button>
<button type="submit" class="btn btn-success rounded-pill">Speichern</button>
<button type="submit" class="btn btn-success rounded-pill">
@(isEditMode ? "Aktualisieren" : "Speichern")
</button>
</div>
</EditForm>
</div>
@ -117,6 +126,7 @@
private List<CustomerModel> customers = new();
private CustomerModel newCustomer = new();
private bool showModal = false;
private bool isEditMode = false;
private bool isLoading = true;
protected override async Task OnInitializedAsync()
@ -134,6 +144,22 @@
private void ShowCreateModal()
{
newCustomer = new CustomerModel();
isEditMode = false;
showModal = true;
}
private void ShowEditModal(CustomerModel c)
{
newCustomer = new CustomerModel
{
Id = c.Id,
Name = c.Name,
Standort = c.Standort,
Ansprechpartner = c.Ansprechpartner,
Telefon = c.Telefon,
Email = c.Email
};
isEditMode = true;
showModal = true;
}
@ -144,16 +170,22 @@
private async Task SaveCustomer()
{
var success = await CustomerService.AddCustomerAsync(newCustomer);
bool success;
if (isEditMode)
success = await CustomerService.UpdateCustomerAsync(newCustomer);
else
success = await CustomerService.AddCustomerAsync(newCustomer);
if (success)
{
await LoadCustomers();
await JS.InvokeVoidAsync("alert", "✅ Kunde erfolgreich angelegt!");
CloseModal();
await JS.InvokeVoidAsync("showToast", "✅ Erfolgreich gespeichert!");
}
else
{
await JS.InvokeVoidAsync("alert", "❌ Fehler beim Speichern.");
await JS.InvokeVoidAsync("showToast", "❌ Fehler beim Speichern.", "error");
}
}
@ -166,11 +198,11 @@
if (success)
{
customers.Remove(customer);
await JS.InvokeVoidAsync("alert", "🗑️ Kunde gelöscht.");
await JS.InvokeVoidAsync("showToast", "🗑️ Kunde gelöscht");
}
else
{
await JS.InvokeVoidAsync("alert", "❌ Fehler beim Löschen.");
await JS.InvokeVoidAsync("showToast", "❌ Fehler beim Löschen.", "error");
}
}
}

View File

@ -76,9 +76,12 @@
customers = await CustomerService.GetCustomersAsync();
}
private async Task SelectCustomer(CustomerModel c)
{
await State.SetCustomerAsync(c.Name);
Nav.NavigateTo("/qrscanner");
}
private async Task SelectCustomer(CustomerModel c)
{
await State.SetCustomerAsync(c.Name);
Nav.NavigateTo($"/stations/{c.Id}");
}
}

View File

@ -1,7 +1,9 @@
@page "/filterform"
@inject IJSRuntime JS
@inject NavigationManager NavManager
@using FilterCair.Shared.Models
@using Microsoft.AspNetCore.Components.Forms
@inject FilterCair.Client.Services.API.FilterService FilterService
<PageTitle>Filterdaten erfassen</PageTitle>
@ -18,20 +20,20 @@
<ValidationSummary />
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">Filter-ID</label>
<InputText class="form-control form-control-lg" @bind-Value="filter.FilterId" placeholder="z. B. FC-A-234" />
<label class="form-label fw-semibold">Bezeichnung</label>
<InputText class="form-control form-control-lg" @bind-Value="filter.Bezeichnung" placeholder="z. B. Filter Nordhalle" />
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Halle</label>
<InputSelect class="form-select form-select-lg" @bind-Value="filter.Halle">
<option value=""> bitte wählen </option>
@foreach (var h in halls)
{
<option value="@h">@h</option>
}
</InputSelect>
<label class="form-label fw-semibold">Typ</label>
<InputText class="form-control form-control-lg" @bind-Value="filter.Typ" placeholder="z. B. Aktivkohle" />
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Seriennummer</label>
<InputText class="form-control form-control-lg" @bind-Value="filter.Seriennummer" placeholder="z. B. SN-2024-0012" />
</div>
<div class="col-md-6">
@ -45,34 +47,20 @@
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Luftdruck (Pa)</label>
<InputNumber class="form-control form-control-lg" @bind-Value="filter.Luftdruck" />
<label class="form-label fw-semibold">Einbaudatum</label>
<InputDate class="form-control form-control-lg" @bind-Value="filter.Einbaudatum" />
</div>
<div class="col-12">
<label class="form-label fw-semibold">Bemerkung</label>
<InputTextArea class="form-control form-control-lg" rows="3" @bind-Value="filter.Bemerkung" />
<div class="col-md-6">
<label class="form-label fw-semibold">Letzte Wartung</label>
<InputDate class="form-control form-control-lg" @bind-Value="filter.LetzteWartung" />
</div>
<!-- 📸 Fotos hinzufügen -->
<div class="col-12">
<label class="form-label fw-semibold">Fotos anhängen</label>
<InputFile OnChange="OnFilesSelected" multiple class="form-control" />
<div class="col-md-6">
<label class="form-label fw-semibold">QR-Code</label>
<InputText class="form-control form-control-lg" @bind-Value="filter.QRCode" placeholder="z. B. FC-Q12345" />
</div>
@if (imagePreviews.Count > 0)
{
<div class="col-12 mt-3">
<div class="d-flex flex-wrap gap-3 justify-content-center">
@foreach (var img in imagePreviews)
{
<div class="image-preview border rounded shadow-sm">
<img src="@img" alt="Foto" class="img-thumbnail" />
</div>
}
</div>
</div>
}
</div>
<div class="d-grid mt-4">
@ -100,48 +88,72 @@
@code {
private FilterModel filter = new();
private List<string> halls = new() { "Halle A", "Halle B", "Halle C" };
private bool showToast;
private List<string> imagePreviews = new();
private async Task OnFilesSelected(InputFileChangeEventArgs e)
{
imagePreviews.Clear();
foreach (var file in e.GetMultipleFiles(3)) // max. 3 Bilder
{
using var stream = file.OpenReadStream(maxAllowedSize: 5_000_000); // 5 MB max
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
var base64 = Convert.ToBase64String(ms.ToArray());
imagePreviews.Add($"data:{file.ContentType};base64,{base64}");
}
StateHasChanged();
}
private async Task SaveForm()
{
Console.WriteLine($"Gespeichert: {System.Text.Json.JsonSerializer.Serialize(filter)}");
await JS.InvokeVoidAsync("console.log", "Filter gespeichert:", filter);
bool success;
showToast = true;
StateHasChanged();
if (filter.Id > 0)
{
// Update vorhandenen Filter
success = await FilterService.UpdateFilterAsync(filter);
}
else
{
// Neuer Filter
success = await FilterService.AddFilterAsync(filter);
}
await Task.Delay(3000);
showToast = false;
StateHasChanged();
if (success)
{
showToast = true;
StateHasChanged();
await Task.Delay(3000);
showToast = false;
StateHasChanged();
await JS.InvokeVoidAsync("showToast", "✅ Filter erfolgreich gespeichert!");
}
else
{
await JS.InvokeVoidAsync("showToast", "❌ Fehler beim Speichern.", "error");
}
}
[Inject] NavigationManager? NavManager { get; set; }
protected override void OnInitialized()
{
var qrParam = NavManager?.ToAbsoluteUri(NavManager.Uri).Query;
if (!string.IsNullOrEmpty(qrParam))
var uri = NavManager.ToAbsoluteUri(NavManager.Uri);
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
var stationIdStr = query.Get("stationId");
if (int.TryParse(stationIdStr, out int stationId))
{
var parts = System.Web.HttpUtility.ParseQueryString(qrParam);
filter.FilterId = parts.Get("id");
filter.StationId = stationId;
}
}
protected override async Task OnInitializedAsync()
{
var uri = NavManager.ToAbsoluteUri(NavManager.Uri);
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
// StationId setzen
if (int.TryParse(query.Get("stationId"), out int stationId))
{
filter.StationId = stationId;
}
// Falls FilterId vorhanden → bestehenden Filter laden
if (int.TryParse(query.Get("filterId"), out int filterId))
{
var existing = await FilterService.GetFilterByIdAsync(filterId);
if (existing != null)
filter = existing;
}
}
}

View File

@ -0,0 +1,87 @@
@page "/stations/{customerId:int}/filters/{stationId:int}"
@using FilterCair.Shared.Models
@inject FilterCair.Client.Services.API.FilterService FilterService
@inject NavigationManager Nav
<PageTitle>Filterübersicht</PageTitle>
<div class="container py-4 fade-in">
<h4 class="text-primary text-center mb-4">
<i class="bi bi-funnel me-2"></i> Filterübersicht
</h4>
@if (isLoading)
{
<div class="text-center text-muted py-5">
<div class="spinner-border text-primary" role="status"></div>
<p class="mt-3">Lade Filter...</p>
</div>
}
else if (filters.Count == 0)
{
<div class="alert alert-info text-center">
Keine Filter für diese Station vorhanden.
</div>
}
else
{
<div class="row g-3 justify-content-center">
@foreach (var f in filters)
{
<div class="col-12 col-md-6 col-lg-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<h5 class="fw-semibold text-primary">
<i class="bi bi-funnel me-1"></i> @f.Bezeichnung
</h5>
<p class="text-muted small mb-1">
<strong>Typ:</strong> @f.Typ
</p>
<p class="text-muted small mb-1">
<strong>Seriennr.:</strong> @f.Seriennummer
</p>
<p class="text-muted small mb-1">
<strong>Zustand:</strong> @f.Zustand
</p>
<p class="text-muted small mb-0">
<strong>Letzte Wartung:</strong>
@(f.LetzteWartung?.ToString("dd.MM.yyyy") ?? "")
</p>
</div>
<div class="card-footer bg-light text-end">
<button class="btn btn-outline-primary btn-sm rounded-pill"
@onclick="() => OpenFilterForm(f)">
<i class="bi bi-pencil"></i> Bearbeiten
</button>
</div>
</div>
</div>
}
</div>
}
</div>
@code {
[Parameter] public int customerId { get; set; }
[Parameter] public int stationId { get; set; }
private List<FilterModel> filters = new();
private bool isLoading = true;
protected override async Task OnInitializedAsync()
{
await LoadFilters();
}
private async Task LoadFilters()
{
isLoading = true;
filters = await FilterService.GetFiltersByStationAsync(stationId);
isLoading = false;
}
private void OpenFilterForm(FilterModel filter)
{
Nav.NavigateTo($"/filterform?filterId={filter.Id}&stationId={stationId}&customerId={customerId}");
}
}

View File

@ -0,0 +1,126 @@
@page "/stations/{customerId:int}"
@using FilterCair.Shared.Models
@inject FilterCair.Client.Services.API.StationService StationService
@inject FilterCair.Client.Services.API.FilterService FilterService
@inject NavigationManager Nav
@inject IJSRuntime JS
<PageTitle>Stationen</PageTitle>
<div class="container py-4">
<h4 class="text-primary text-center mb-4">
<i class="bi bi-geo-alt me-2"></i> Stationen der Fabrik
</h4>
<!-- QR Scanner -->
<div class="text-center mb-4">
<video id="video" autoplay playsinline class="border rounded shadow-sm"
style="width:100%;max-width:420px;"></video>
@if (!string.IsNullOrEmpty(scanResult))
{
<div class="alert alert-success mt-3">
<strong>QR erkannt:</strong> @scanResult
</div>
}
<div class="mt-3">
<button class="btn btn-secondary me-2" @onclick="StartScan">🔄 Neu starten</button>
<button class="btn btn-outline-danger" @onclick="StopScan">⏹ Stop</button>
</div>
</div>
<hr />
<!-- Stationsliste -->
@if (isLoading)
{
<div class="text-center text-muted py-5">
<div class="spinner-border text-primary" role="status"></div>
<p class="mt-3">Lade Stationen...</p>
</div>
}
else if (stations.Count == 0)
{
<div class="alert alert-info text-center">
Keine Stationen gefunden.
</div>
}
else
{
<div class="row g-3 justify-content-center">
@foreach (var s in stations)
{
<div class="col-12 col-md-6 col-lg-4">
<div class="card shadow-sm border-0 station-card h-100"
@onclick="() => SelectStation(s)">
<div class="card-body">
<h5 class="fw-semibold text-primary mb-2">
<i class="bi bi-building me-1"></i> @s.Name
</h5>
<p class="text-muted small mb-1">
<i class="bi bi-geo me-1"></i> @s.Standort
</p>
<p class="text-muted small mb-0">@s.Beschreibung</p>
</div>
</div>
</div>
}
</div>
}
</div>
@code {
[Parameter] public int customerId { get; set; }
private List<StationModel> stations = new();
private string? scanResult;
private bool isLoading = true;
protected override async Task OnInitializedAsync()
{
await LoadStations();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("QRScanner.init", DotNetObjectReference.Create(this));
}
}
private async Task LoadStations()
{
try
{
isLoading = true;
stations = await StationService.GetStationsByCustomerAsync(customerId);
}
catch (Exception ex)
{
Console.WriteLine($"❌ Fehler beim Laden der Stationen: {ex.Message}");
}
finally
{
isLoading = false;
}
}
private void SelectStation(StationModel s)
{
Nav.NavigateTo($"/stations/{customerId}/filters/{s.Id}");
}
private async Task StartScan() => await JS.InvokeVoidAsync("QRScanner.start");
private async Task StopScan() => await JS.InvokeVoidAsync("QRScanner.stop");
[JSInvokable]
public void OnQrDetected(string code)
{
scanResult = code;
// TODO: später Filter anhand QR-Code öffnen
// Nav.NavigateTo($"/stations/{customerId}/filterdetail/{code}");
StateHasChanged();
}
}

View File

@ -1,8 +1,9 @@
using FilterCair.Client;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using FilterCair.Client.Services;
using FilterCair.Client.Services.API;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
@ -26,6 +27,8 @@ builder.Services.AddMsalAuthentication(options =>
builder.Services.AddScoped<AppState>();
//API Services
builder.Services.AddScoped<FilterCair.Client.Services.API.CustomerService>();
builder.Services.AddScoped<CustomerService>();
builder.Services.AddScoped<StationService>();
builder.Services.AddScoped<FilterService>();
await builder.Build().RunAsync();

View File

@ -57,5 +57,19 @@ namespace FilterCair.Client.Services.API
return false;
}
}
public async Task<bool> UpdateCustomerAsync(CustomerModel customer)
{
try
{
var response = await _http.PutAsJsonAsync($"{BaseUrl}/{customer.Id}", customer);
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
Console.WriteLine($"❌ Fehler beim Aktualisieren: {ex.Message}");
return false;
}
}
}
}

View File

@ -0,0 +1,96 @@
using System.Net.Http.Json;
using FilterCair.Shared.Models;
namespace FilterCair.Client.Services.API
{
public class FilterService
{
private readonly HttpClient _http;
private readonly IConfiguration _config;
private string BaseUrl => _config["Api:BaseUrl"] + "/api/Filter";
public FilterService(HttpClient http, IConfiguration config)
{
_http = http;
_config = config;
}
// Alle Filter einer Station laden
public async Task<List<FilterModel>> GetFiltersByStationAsync(int stationId)
{
try
{
var filters = await _http.GetFromJsonAsync<List<FilterModel>>($"{BaseUrl}/byStation/{stationId}");
return filters ?? new();
}
catch (Exception ex)
{
Console.WriteLine($"❌ Fehler beim Laden der Filter: {ex.Message}");
return new();
}
}
// Filter anlegen
public async Task<bool> AddFilterAsync(FilterModel filter)
{
try
{
var response = await _http.PostAsJsonAsync(BaseUrl, filter);
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
Console.WriteLine($"❌ Fehler beim Erstellen: {ex.Message}");
return false;
}
}
// Filter per QR-Code abrufen
public async Task<FilterModel?> GetFilterByQRCodeAsync(string qrCode)
{
try
{
var result = await _http.GetFromJsonAsync<FilterModel>($"{BaseUrl}/byQr/{qrCode}");
return result;
}
catch (Exception ex)
{
Console.WriteLine($"❌ Fehler beim Laden per QR-Code: {ex.Message}");
return null;
}
}
// Einzelnen Filter per ID laden
public async Task<FilterModel?> GetFilterByIdAsync(int id)
{
try
{
var result = await _http.GetFromJsonAsync<FilterModel>($"{BaseUrl}/{id}");
return result;
}
catch (Exception ex)
{
Console.WriteLine($"❌ Fehler beim Laden des Filters: {ex.Message}");
return null;
}
}
// Filter aktualisieren
public async Task<bool> UpdateFilterAsync(FilterModel filter)
{
try
{
var response = await _http.PutAsJsonAsync($"{BaseUrl}/{filter.Id}", filter);
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
Console.WriteLine($"❌ Fehler beim Aktualisieren: {ex.Message}");
return false;
}
}
}
}

View File

@ -0,0 +1,48 @@
using System.Net.Http.Json;
using FilterCair.Shared.Models;
namespace FilterCair.Client.Services.API
{
public class StationService
{
private readonly HttpClient _http;
private readonly IConfiguration _config;
private string BaseUrl => _config["Api:BaseUrl"] + "/api/Station";
public StationService(HttpClient http, IConfiguration config)
{
_http = http;
_config = config;
}
// 🔹 Alle Stationen eines Kunden laden
public async Task<List<StationModel>> GetStationsByCustomerAsync(int customerId)
{
try
{
var stations = await _http.GetFromJsonAsync<List<StationModel>>($"{BaseUrl}/byCustomer/{customerId}");
return stations ?? new();
}
catch (Exception ex)
{
Console.WriteLine($"❌ Fehler beim Laden der Stationen: {ex.Message}");
return new();
}
}
// 🔹 Neue Station anlegen
public async Task<bool> AddStationAsync(StationModel station)
{
try
{
var response = await _http.PostAsJsonAsync(BaseUrl, station);
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
Console.WriteLine($"❌ Fehler beim Erstellen: {ex.Message}");
return false;
}
}
}
}

View File

@ -43,6 +43,7 @@
<script src="js/bootstrap.bundle.min.js"></script>
<script src="js/jsQR.js"></script>
<script src="js/filtercair.js"></script>
<script src="js/toastcontainer.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>

View File

@ -0,0 +1,30 @@
window.showToast = (message, type = "success") => {
const container = document.querySelector(".toast-container") || createToastContainer();
const toastEl = document.createElement("div");
toastEl.className = `toast align-items-center text-white border-0 mb-2 bg-${type === "error" ? "danger" :
type === "warning" ? "warning" :
type === "info" ? "info" : "success"
}`;
toastEl.role = "alert";
toastEl.innerHTML = `
<div class="d-flex">
<div class="toast-body fw-semibold">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
container.appendChild(toastEl);
const toast = new bootstrap.Toast(toastEl, { delay: 3000 });
toast.show();
toastEl.addEventListener("hidden.bs.toast", () => toastEl.remove());
};
function createToastContainer() {
const container = document.createElement("div");
container.className = "toast-container position-fixed bottom-0 end-0 p-3";
document.body.appendChild(container);
return container;
}

View File

@ -19,7 +19,7 @@ builder.Services.AddCors(options =>
{
options.AddPolicy("AllowClient", policy =>
{
policy.WithOrigins("https://filtercair-client-efava4bfgvamhkfu.westeurope-01.azurewebsites.net", "https://localhost:5001")
policy.WithOrigins("https://filtercair-client-efava4bfgvamhkfu.westeurope-01.azurewebsites.net", "https://localhost:5186", "http://localhost:5186")
.AllowAnyHeader()
.AllowAnyMethod();
});

View File

@ -9,14 +9,14 @@ 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; }
public int Id { get; set; }
public int StationId { get; set; }
public string Bezeichnung { get; set; } = string.Empty;
public string Typ { get; set; } = string.Empty;
public string Seriennummer { get; set; } = string.Empty;
public DateTime? Einbaudatum { get; set; }
public DateTime? LetzteWartung { get; set; }
public string Zustand { get; set; } = string.Empty;
public string QRCode { get; set; } = string.Empty;
}
}