Funktionierende Customers ueber API mit Database Sync

This commit is contained in:
Marc Wieland 2025-11-05 15:01:39 +01:00
parent 13a0dbb9bc
commit c4ad142e4f
11 changed files with 180 additions and 90 deletions

View File

@ -1,6 +1,8 @@
@page "/admin/customers"
@layout AdminLayout
@using FilterCair.Shared.Models
@inject FilterCair.Client.Services.API.CustomerService CustomerService
@inject IJSRuntime JS
<PageTitle>Kunden verwalten</PageTitle>
@ -14,8 +16,14 @@
</button>
</div>
<!-- Kundenliste -->
@if (customers.Count == 0)
@if (isLoading)
{
<div class="text-center py-5 text-muted">
<div class="spinner-border text-primary" role="status"></div>
<p class="mt-3">Lade Kunden...</p>
</div>
}
else if (customers.Count == 0)
{
<div class="alert alert-info text-center">
Keine Kunden vorhanden. Lege den ersten Kunden an!
@ -45,7 +53,8 @@
<td>@c.Telefon</td>
<td>@c.Email</td>
<td class="text-end">
<button class="btn btn-outline-danger btn-sm rounded-pill" @onclick="() => DeleteCustomer(c)">
<button class="btn btn-outline-danger btn-sm rounded-pill"
@onclick="() => DeleteCustomer(c)">
<i class="bi bi-trash"></i>
</button>
</td>
@ -75,34 +84,26 @@
<label class="form-label">Name</label>
<InputText class="form-control" @bind-Value="newCustomer.Name" required />
</div>
<div class="mb-3">
<label class="form-label">Standort</label>
<InputText class="form-control" @bind-Value="newCustomer.Standort" required />
</div>
<div class="mb-3">
<label class="form-label">Ansprechpartner</label>
<InputText class="form-control" @bind-Value="newCustomer.Ansprechpartner" />
</div>
<div class="mb-3">
<label class="form-label">Telefon</label>
<InputText class="form-control" @bind-Value="newCustomer.Telefon" />
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<InputText class="form-control" @bind-Value="newCustomer.Email" />
</div>
<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="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>
</div>
</EditForm>
</div>
@ -116,15 +117,18 @@
private List<CustomerModel> customers = new();
private CustomerModel newCustomer = new();
private bool showModal = false;
private bool isLoading = true;
protected override void OnInitialized()
protected override async Task OnInitializedAsync()
{
// TODO: später über API laden
customers = new()
{
new() { Id = 1, Name = "Freudenberg Weinheim", Standort = "Weinheim", Ansprechpartner = "Marc Wieland", Telefon = "+49 6201 80 1234", Email = "marc.wieland@freudenberg-pm.com" },
new() { Id = 2, Name = "CleanAir Solutions", Standort = "Mannheim", Ansprechpartner = "Tom Fischer", Telefon = "+49 621 555 123" , Email = "FischersTom@cleanair.de"}
};
await LoadCustomers();
}
private async Task LoadCustomers()
{
isLoading = true;
customers = await CustomerService.GetCustomersAsync();
isLoading = false;
}
private void ShowCreateModal()
@ -138,15 +142,35 @@
showModal = false;
}
private void SaveCustomer()
private async Task SaveCustomer()
{
newCustomer.Id = customers.Count + 1;
customers.Add(newCustomer);
showModal = false;
var success = await CustomerService.AddCustomerAsync(newCustomer);
if (success)
{
await LoadCustomers();
await JS.InvokeVoidAsync("alert", "✅ Kunde erfolgreich angelegt!");
CloseModal();
}
else
{
await JS.InvokeVoidAsync("alert", "❌ Fehler beim Speichern.");
}
}
private void DeleteCustomer(CustomerModel customer)
private async Task DeleteCustomer(CustomerModel customer)
{
customers.Remove(customer);
bool confirmed = await JS.InvokeAsync<bool>("confirm", $"Soll der Kunde '{customer.Name}' wirklich gelöscht werden?");
if (!confirmed) return;
var success = await CustomerService.DeleteCustomerAsync(customer.Id);
if (success)
{
customers.Remove(customer);
await JS.InvokeVoidAsync("alert", "🗑️ Kunde gelöscht.");
}
else
{
await JS.InvokeVoidAsync("alert", "❌ Fehler beim Löschen.");
}
}
}

View File

@ -1,13 +1,13 @@
@page "/customers"
@using FilterCair.Shared.Models
@inject FilterCair.Client.Services.API.CustomerService CustomerService
@inject IJSRuntime JS
@inject NavigationManager Nav
@inject FilterCair.Client.Services.AppState State
<h3>Kundenübersicht</h3>
<div class="container py-4 fade-in">
<h4 class="mb-4 text-center text-primary">
<i class="bi bi-buildings me-2"></i> Kunden & Fabriken
@ -23,62 +23,62 @@
<!-- 🏭 Kundenliste -->
<div class="row g-4 justify-content-center">
@foreach (var c in FilteredCustomers)
@if (customers == null)
{
<div class="col-12 col-sm-6 col-lg-4">
<div class="card customer-card border-0 shadow-sm h-100"
@onclick="@(() => SelectCustomer(c))">
<div class="card-body">
<h5 class="fw-semibold mb-2 text-primary">
<i class="bi bi-building me-1"></i> @c.Name
</h5>
<p class="text-muted mb-1">
<i class="bi bi-geo-alt me-1"></i> @c.Standort
</p>
<p class="text-muted small mb-0">
<i class="bi bi-person me-1"></i> @c.Ansprechpartner
</p>
<p class="text-center text-muted">Lade Kunden...</p>
}
else
{
@foreach (var c in FilteredCustomers)
{
<div class="col-12 col-sm-6 col-lg-4">
<div class="card customer-card border-0 shadow-sm h-100"
@onclick="@(() => SelectCustomer(c))">
<div class="card-body">
<h5 class="fw-semibold mb-2 text-primary">
<i class="bi bi-building me-1"></i> @c.Name
</h5>
<p class="text-muted mb-1">
<i class="bi bi-geo-alt me-1"></i> @c.Standort
</p>
<p class="text-muted small mb-0">
<i class="bi bi-person me-1"></i> @c.Ansprechpartner
</p>
</div>
</div>
</div>
</div>
}
@if (!FilteredCustomers.Any())
{
<div class="text-center text-muted mt-5">
<i class="bi bi-search display-5 d-block mb-2"></i>
Keine passenden Kunden gefunden.
</div>
}
}
</div>
@if (FilteredCustomers.Count() == 0)
{
<div class="text-center text-muted mt-5">
<i class="bi bi-search display-5 d-block mb-2"></i>
Keine passenden Kunden gefunden.
</div>
}
</div>
@code {
private List<CustomerModel> customers = new();
private List<CustomerModel>? customers;
private string searchText = "";
private IEnumerable<CustomerModel> FilteredCustomers =>
string.IsNullOrWhiteSpace(searchText)
? customers
: customers.Where(c =>
c.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
c.Standort.Contains(searchText, StringComparison.OrdinalIgnoreCase));
? customers ?? new List<CustomerModel>()
: customers!.Where(c =>
c.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
c.Standort.Contains(searchText, StringComparison.OrdinalIgnoreCase));
protected override void OnInitialized()
protected override async Task OnInitializedAsync()
{
// später aus API laden
customers = new()
{
new() { Id = 1, Name = "Freudenberg Weinheim", Standort = "Weinheim", Ansprechpartner = "Marc Wieland", Telefon = "+49 6201 80 1234" },
new() { Id = 2, Name = "FilterTech GmbH", Standort = "Kaiserslautern", Ansprechpartner = "Lena Becker", Telefon = "+49 631 987654" },
new() { Id = 3, Name = "CleanAir Solutions", Standort = "Mannheim", Ansprechpartner = "Tom Fischer", Telefon = "+49 621 555 123" }
};
customers = await CustomerService.GetCustomersAsync();
}
private async Task SelectCustomer(CustomerModel c)
{
await State.SetCustomerAsync(c.Name);
Nav.NavigateTo("/qrscanner");
Nav.NavigateTo("/qrscanner");
}
}
}

View File

@ -25,4 +25,7 @@ builder.Services.AddMsalAuthentication(options =>
//Services
builder.Services.AddScoped<AppState>();
//API Services
builder.Services.AddScoped<FilterCair.Client.Services.API.CustomerService>();
await builder.Build().RunAsync();

View File

@ -0,0 +1,61 @@
using FilterCair.Shared.Models;
using System.Net.Http.Json;
namespace FilterCair.Client.Services.API
{
public class CustomerService
{
private readonly HttpClient _http;
private readonly IConfiguration _config;
private string BaseUrl => _config["Api:BaseUrl"] + "/api/Customer";
public CustomerService(HttpClient http, IConfiguration config)
{
_http = http;
_config = config;
}
public async Task<List<CustomerModel>> GetCustomersAsync()
{
try
{
var result = await _http.GetFromJsonAsync<List<CustomerModel>>(BaseUrl);
return result ?? new();
}
catch (Exception ex)
{
Console.WriteLine($"❌ Fehler beim Laden der Kunden: {ex.Message}");
return new();
}
}
public async Task<bool> AddCustomerAsync(CustomerModel customer)
{
try
{
var response = await _http.PostAsJsonAsync(BaseUrl, customer);
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
Console.WriteLine($"❌ Fehler beim Anlegen: {ex.Message}");
return false;
}
}
public async Task<bool> DeleteCustomerAsync(int id)
{
try
{
var response = await _http.DeleteAsync($"{BaseUrl}/{id}");
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
Console.WriteLine($"❌ Fehler beim Löschen: {ex.Message}");
return false;
}
}
}
}

View File

@ -14,6 +14,6 @@
]
},
"Api": {
"BaseUrl": "https://filtercair-api.azurewebsites.net"
"BaseUrl": "https://filtercair-server-gwctc2gbf5f4axgj.westeurope-01.azurewebsites.net"
}
}

View File

@ -1,17 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
namespace FilterCair.Server.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class TestController : ControllerBase
{
[HttpGet("public")]
public IActionResult GetPublic() => Ok("✅ Öffentlich erreichbar keine Auth nötig");
[Authorize]
[HttpGet("protected")]
public IActionResult GetProtected() => Ok("🔒 Du bist authentifiziert über Azure AD!");
}
}

View File

@ -10,7 +10,7 @@
$ErrorActionPreference = "Stop"
# Variables
$projectPath = "C:\DEVQPDC\Masterarbeit\filtercair_dev\Masterarbeit_New\FilterCair.Server"
$projectPath = "C:\DEVQPDC\FilterCairStack\FilterCair.Server"
$publishFolder = "$projectPath\bin\Release\net9.0\linux-x64\publish"
$zipFile = "$projectPath\publish.zip"
$resourceGroup = "FilterCair-RG"

View File

@ -9,14 +9,24 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.10" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.10">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Identity.Web" Version="4.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="9.0.6" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.6" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FilterCair.Shared\FilterCair.Shared.csproj" />
<ItemGroup>
<ProjectReference Include="..\FilterCair.Shared\FilterCair.Shared.csproj" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,6 @@
using FilterCair.Server.Data;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
@ -6,11 +8,18 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddDbContext<FilterCairContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"),
sql => sql.EnableRetryOnFailure(5, TimeSpan.FromSeconds(10), null))
);
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowClient", policy =>
{
policy.WithOrigins("https://filtercair-client.azurewebsites.net", "https://localhost:5001")
policy.WithOrigins("https://filtercair-client-efava4bfgvamhkfu.westeurope-01.azurewebsites.net", "https://localhost:5001")
.AllowAnyHeader()
.AllowAnyMethod();
});

View File

@ -8,7 +8,7 @@
"SignedOutCallbackPath": "/authentication/logout-callback"
},
"ConnectionStrings": {
"DefaultConnection": "Server=tcp:filtercair-sql.database.windows.net,1433;Initial Catalog=FilterCairDB;User ID=sqladmin;Password=SuperSicher123!;Encrypt=True;"
"DefaultConnection": "Server=tcp:filtercair-azure-sql-server.database.windows.net,1433;Initial Catalog=FilterCairDB;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;Authentication=Active Directory Managed Identity;"
},
"Logging": {
"LogLevel": {

Binary file not shown.