Compare commits
13 Commits
195e0a7b45
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1604775fc | ||
|
|
92e92142ce | ||
|
|
fda4543775 | ||
|
|
0c2a83a31f | ||
| 2927131f9e | |||
| 8e764cbc63 | |||
| 73a8ec0e76 | |||
|
|
371d8d9058 | ||
|
|
d5fe1fc04c | ||
|
|
0bf240ccda | ||
|
|
c4ad142e4f | ||
|
|
13a0dbb9bc | ||
|
|
efda608c4f |
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
|
||||
51
FilterCair.Client/Layout/AdminLayout.razor
Normal file
51
FilterCair.Client/Layout/AdminLayout.razor
Normal file
@@ -0,0 +1,51 @@
|
||||
@inherits LayoutComponentBase
|
||||
@inject NavigationManager Nav
|
||||
|
||||
<div class="d-flex flex-column min-vh-100 bg-light">
|
||||
|
||||
<!-- 🔹 Admin Navbar -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark" style="background-color:#003d66;">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand fw-semibold" href="/admin">
|
||||
<i class="bi bi-gear-fill me-2"></i> Adminbereich
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#adminNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="adminNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link text-white" href="/admin/customers" Match="NavLinkMatch.All">
|
||||
<i class="bi bi-buildings me-1"></i> Kunden
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link text-white" href="/admin/filters">
|
||||
<i class="bi bi-funnel me-1"></i> Filter
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link text-white" href="/admin/tasks">
|
||||
<i class="bi bi-list-check me-1"></i> Aufgaben
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button class="btn btn-outline-light btn-sm" @onclick="@(() => Nav.NavigateTo("/"))">
|
||||
<i class="bi bi-arrow-left me-1"></i> Zurück
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 🔹 Body -->
|
||||
<main class="flex-fill container py-4">
|
||||
@Body
|
||||
</main>
|
||||
|
||||
<footer class="text-center text-secondary small py-2 mt-auto border-top">
|
||||
@DateTime.Now.Year FilterCair • Adminbereich
|
||||
</footer>
|
||||
</div>
|
||||
@@ -1,14 +1,19 @@
|
||||
@inherits LayoutComponentBase
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||
@inject FilterCair.Client.Services.AppState State
|
||||
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="d-flex flex-column min-vh-100">
|
||||
|
||||
<!-- 🔹 NAVBAR -->
|
||||
<nav class="navbar navbar-expand-lg shadow-sm main-nav">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand fw-semibold text-white" href="/">
|
||||
<i class="bi bi-wind me-1"></i> FilterCair
|
||||
<a class="navbar-brand fw-semibold text-white d-flex align-items-center gap-2" href="/">
|
||||
<i class="bi bi-wind me-1"></i>
|
||||
FilterCair
|
||||
<span class="status-dot" id="onlineStatusDot">●</span>
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
@@ -22,18 +27,22 @@
|
||||
<i class="bi bi-house-door me-1"></i> Start
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link text-white" href="/qrscanner">
|
||||
<i class="bi bi-qr-code-scan me-1"></i> QR-Scanner
|
||||
<NavLink class="nav-link text-white" href="/tasks">
|
||||
<i class="bi bi-list-check me-1"></i> Tasklist
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link text-white" href="/filterform">
|
||||
<i class="bi bi-funnel me-1"></i> Filter-Form
|
||||
<NavLink class="nav-link text-white" href="/customers">
|
||||
<i class="bi bi-buildings me-1"></i> Kunden
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
<!-- 🔐 Login/Logout Bereich -->
|
||||
<AuthorizeView>
|
||||
<Authorized Context="auth">
|
||||
@@ -65,6 +74,86 @@
|
||||
|
||||
<!-- 🔹 FOOTER -->
|
||||
<footer class="footer text-center small text-secondary py-2 mt-auto border-top">
|
||||
© @DateTime.Now.Year FilterCair – FPM Service GmbH
|
||||
@if (!string.IsNullOrEmpty(State.SelectedCustomer))
|
||||
{
|
||||
<div class="text-muted small mb-1">
|
||||
<i class="bi bi-geo-alt me-1 text-primary"></i>
|
||||
Aktiver Kunde: <strong>@State.SelectedCustomer</strong>
|
||||
</div>
|
||||
}
|
||||
@DateTime.Now.Year FilterCair - FPM Service GmbH
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
@code{
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await State.LoadFromSessionAsync();
|
||||
State.OnChange += StateHasChanged;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
State.OnChange -= StateHasChanged;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await JS.InvokeVoidAsync("eval", @"
|
||||
// Warte auf filterDb
|
||||
const waitForFilterDb = () => new Promise(resolve => {
|
||||
const check = () => window.filterDbReady ? resolve() : setTimeout(check, 50);
|
||||
check();
|
||||
});
|
||||
|
||||
// === ONLINE STATUS DOT ===
|
||||
async function updateStatus() {
|
||||
try {
|
||||
await waitForFilterDb();
|
||||
const dot = document.getElementById('onlineStatusDot');
|
||||
if (!dot) return;
|
||||
|
||||
if (navigator.onLine) {
|
||||
dot.classList.remove('offline');
|
||||
dot.classList.add('online');
|
||||
dot.title = 'Online – alles sync!';
|
||||
} else {
|
||||
dot.classList.remove('online');
|
||||
dot.classList.add('offline');
|
||||
dot.title = 'Offline – Änderungen werden gespeichert';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('updateStatus Fehler:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// === AUTO SYNC ===
|
||||
let isSyncing = false;
|
||||
async function startSync() {
|
||||
if (isSyncing || !navigator.onLine) return;
|
||||
isSyncing = true;
|
||||
try {
|
||||
await waitForFilterDb();
|
||||
await window.filterDb.syncOutbox();
|
||||
console.log('Sync erfolgreich');
|
||||
updateStatus(); // Sync → Status aktualisieren
|
||||
} catch (err) {
|
||||
console.error('Sync Fehler:', err);
|
||||
}
|
||||
isSyncing = false;
|
||||
}
|
||||
|
||||
// === INITIALISIERUNG ===
|
||||
updateStatus();
|
||||
setInterval(updateStatus, 2000);
|
||||
window.addEventListener('online', () => { updateStatus(); startSync(); });
|
||||
window.addEventListener('offline', updateStatus);
|
||||
setInterval(() => { if (navigator.onLine) startSync(); }, 30000);
|
||||
if (navigator.onLine) startSync();
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
34
FilterCair.Client/Pages/Admin/AdminHome.razor
Normal file
34
FilterCair.Client/Pages/Admin/AdminHome.razor
Normal file
@@ -0,0 +1,34 @@
|
||||
@page "/admin"
|
||||
@layout AdminLayout
|
||||
|
||||
<PageTitle>Adminbereich</PageTitle>
|
||||
|
||||
<div class="container py-4 text-center fade-in">
|
||||
<h4 class="mb-4 text-primary">
|
||||
<i class="bi bi-shield-lock me-2"></i> Adminbereich
|
||||
</h4>
|
||||
|
||||
<div class="row g-4 justify-content-center">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card shadow-sm border-0 admin-tile" @onclick="@(() => Nav.NavigateTo("/admin/customers"))">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-buildings display-6 text-primary mb-3"></i>
|
||||
<h5 class="fw-semibold">Kunden verwalten</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card shadow-sm border-0 admin-tile" @onclick="@(() => Nav.NavigateTo("/admin/users"))">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-person display-6 text-primary mb-3"></i>
|
||||
<h5 class="fw-semibold">Benutzer verwalten</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] NavigationManager Nav { get; set; } = default!;
|
||||
}
|
||||
208
FilterCair.Client/Pages/Admin/Customers.razor
Normal file
208
FilterCair.Client/Pages/Admin/Customers.razor
Normal file
@@ -0,0 +1,208 @@
|
||||
@page "/admin/customers"
|
||||
@layout AdminLayout
|
||||
@using FilterCair.Shared.Models
|
||||
@inject FilterCair.Client.Services.API.CustomerService CustomerService
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<PageTitle>Kunden verwalten</PageTitle>
|
||||
|
||||
<div class="container py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-3">
|
||||
<h4 class="text-primary mb-0">
|
||||
<i class="bi bi-buildings me-2"></i> Kundenverwaltung
|
||||
</h4>
|
||||
<button class="btn btn-success rounded-pill" @onclick="ShowCreateModal">
|
||||
<i class="bi bi-plus-lg me-1"></i> Neuer Kunde
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@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!
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped align-middle shadow-sm">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Standort</th>
|
||||
<th>Ansprechpartner</th>
|
||||
<th>Telefon</th>
|
||||
<th>Email</th>
|
||||
<th style="width:160px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var c in customers)
|
||||
{
|
||||
<tr>
|
||||
<td>@c.Name</td>
|
||||
<td>@c.Standort</td>
|
||||
<td>@c.Ansprechpartner</td>
|
||||
<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>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Modal -->
|
||||
@if (showModal)
|
||||
{
|
||||
<div class="modal fade show d-block" tabindex="-1" style="background-color:rgba(0,0,0,0.5);">
|
||||
<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 @(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">
|
||||
<EditForm Model="@newCustomer" OnValidSubmit="SaveCustomer">
|
||||
<DataAnnotationsValidator />
|
||||
<ValidationSummary />
|
||||
|
||||
<div class="mb-3">
|
||||
<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">
|
||||
@(isEditMode ? "Aktualisieren" : "Speichern")
|
||||
</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
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()
|
||||
{
|
||||
await LoadCustomers();
|
||||
}
|
||||
|
||||
private async Task LoadCustomers()
|
||||
{
|
||||
isLoading = true;
|
||||
customers = await CustomerService.GetCustomersAsync();
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void CloseModal()
|
||||
{
|
||||
showModal = false;
|
||||
}
|
||||
|
||||
private async Task SaveCustomer()
|
||||
{
|
||||
bool success;
|
||||
|
||||
if (isEditMode)
|
||||
success = await CustomerService.UpdateCustomerAsync(newCustomer);
|
||||
else
|
||||
success = await CustomerService.AddCustomerAsync(newCustomer);
|
||||
|
||||
if (success)
|
||||
{
|
||||
await LoadCustomers();
|
||||
CloseModal();
|
||||
await JS.InvokeVoidAsync("showToast", "✅ Erfolgreich gespeichert!");
|
||||
}
|
||||
else
|
||||
{
|
||||
await JS.InvokeVoidAsync("showToast", "❌ Fehler beim Speichern.", "error");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteCustomer(CustomerModel 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("showToast", "🗑️ Kunde gelöscht");
|
||||
}
|
||||
else
|
||||
{
|
||||
await JS.InvokeVoidAsync("showToast", "❌ Fehler beim Löschen.", "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
308
FilterCair.Client/Pages/Admin/Users.razor
Normal file
308
FilterCair.Client/Pages/Admin/Users.razor
Normal file
@@ -0,0 +1,308 @@
|
||||
@page "/admin/users"
|
||||
@layout AdminLayout
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<PageTitle>Benutzerverwaltung</PageTitle>
|
||||
|
||||
<style>
|
||||
/* Responsives Verhalten optimieren */
|
||||
.users-table-wrapper {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.users-table {
|
||||
min-width: 900px; /* verhindert hässliches Zusammenstauchen auf kleinen Bildschirmen */
|
||||
}
|
||||
|
||||
@@media (max-width: 992px) {
|
||||
.users-table th:nth-child(2),
|
||||
.users-table td:nth-child(2),
|
||||
.users-table th:nth-child(3),
|
||||
.users-table td:nth-child(3) {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-3">
|
||||
<h4 class="text-primary mb-0">
|
||||
<i class="bi bi-people me-2"></i> Benutzerverwaltung
|
||||
</h4>
|
||||
<button class="btn btn-success rounded-pill" @onclick="ShowCreateModal">
|
||||
<i class="bi bi-plus-lg me-1"></i> Neuer Benutzer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (isLoading)
|
||||
{
|
||||
<div class="text-center py-5 text-muted">
|
||||
<div class="spinner-border text-primary" role="status"></div>
|
||||
<p class="mt-3">Lade Benutzer...</p>
|
||||
</div>
|
||||
}
|
||||
else if (users.Count == 0)
|
||||
{
|
||||
<div class="alert alert-info text-center">
|
||||
Keine Benutzer vorhanden. Lege den ersten Benutzer an!
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="users-table-wrapper">
|
||||
<table class="table table-striped align-middle shadow-sm users-table">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th>Benutzername</th>
|
||||
<th>Vorname</th>
|
||||
<th>Nachname</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Rolle</th>
|
||||
<th style="width: 80px;">Aktiv</th>
|
||||
<th style="width: 140px; text-align: end;">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var u in users)
|
||||
{
|
||||
<tr>
|
||||
<td><strong>@u.UserName</strong></td>
|
||||
<td>@u.FirstName</td>
|
||||
<td>@u.LastName</td>
|
||||
<td>@u.Email</td>
|
||||
<td>
|
||||
<span class="badge @(u.Role == "Administrator" ? "bg-danger" : "bg-secondary")">
|
||||
@u.Role
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<i class="bi @(u.IsActive ? "bi-check-circle-fill text-success fs-5" : "bi-x-circle-fill text-danger fs-5")"></i>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-outline-primary btn-sm rounded-pill me-1"
|
||||
@onclick="() => ShowEditModal(u)"
|
||||
title="Bearbeiten">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm rounded-pill"
|
||||
@onclick="() => DeleteUser(u)"
|
||||
title="Löschen">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Modal mit Backdrop-Click & ESC-Support -->
|
||||
@if (showModal)
|
||||
{
|
||||
<div class="modal fade show d-block" tabindex="-1"
|
||||
style="background-color: rgba(0,0,0,0.5);"
|
||||
@onkeydown="OnModalKeyDown"
|
||||
@onkeydown:preventDefault="true"
|
||||
@onkeydown:stopPropagation="true"
|
||||
@onclick="CloseModal"
|
||||
>
|
||||
|
||||
<!-- Backdrop-Click schließt Modal -->
|
||||
<div class="modal-dialog modal-dialog-centered" @onclick:stopPropagation>
|
||||
<div class="modal-content border-0 shadow-lg rounded-4">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title">
|
||||
<i class="me-2 @(isEditMode ? "bi bi-pencil" : "bi bi-person-plus")"></i>
|
||||
@(isEditMode ? "Benutzer bearbeiten" : "Neuer Benutzer")
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" @onclick="CloseModal" aria-label="Schließen"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<EditForm Model="@currentUser" OnValidSubmit="SaveUser">
|
||||
<DataAnnotationsValidator />
|
||||
ge
|
||||
<ValidationSummary class="text-danger mb-3" />
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Benutzername *</label>
|
||||
<InputText class="form-control" @bind-Value="currentUser.UserName" required />
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Vorname</label>
|
||||
<InputText class="form-control" @bind-Value="currentUser.FirstName" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Nachname</label>
|
||||
<InputText class="form-control" @bind-Value="currentUser.LastName" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 mt-3">
|
||||
<label class="form-label">E-Mail *</label>
|
||||
<InputText type="email" class="form-control" @bind-Value="currentUser.Email" required />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
Passwort @(isEditMode ? "(leer = unverändert)" : "*")
|
||||
</label>
|
||||
<InputText type="password"
|
||||
class="form-control"
|
||||
@bind-Value="currentUser.Password"
|
||||
placeholder="@(isEditMode ? "Leer lassen = Passwort bleibt gleich" : "Pflichtfeld beim Anlegen")"
|
||||
required="@(!isEditMode)" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Rolle</label>
|
||||
<InputSelect class="form-select" @bind-Value="currentUser.Role">
|
||||
<option value="User">User</option>
|
||||
<option value="Administrator">Administrator</option>
|
||||
</InputSelect>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-4">
|
||||
<InputCheckbox class="form-check-input" @bind-Value="currentUser.IsActive" />
|
||||
<label class="form-check-label">Account aktiv</label>
|
||||
</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">
|
||||
@(isEditMode ? "Aktualisieren" : "Anlegen")
|
||||
</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@code {
|
||||
private List<AppUser> users = new();
|
||||
private AppUser currentUser = new();
|
||||
private bool showModal = false;
|
||||
private bool isEditMode = false;
|
||||
private bool isLoading = true;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Demo-Daten direkt beim Start
|
||||
users = new List<AppUser>
|
||||
{
|
||||
new AppUser { Id = 1, UserName = "admin", FirstName = "Max", LastName = "Mustermann", Email = "admin@filtercair.de", Role = "Administrator", IsActive = true },
|
||||
new AppUser { Id = 2, UserName = "peter", FirstName = "Peter", LastName = "Parker", Email = "peter@company.com", Role = "User", IsActive = true },
|
||||
new AppUser { Id = 3, UserName = "susi", FirstName = "Susanne", LastName = "Müller", Email = "susi@company.com", Role = "User", IsActive = false },
|
||||
new AppUser { Id = 4, UserName = "tom", FirstName = "Tom", LastName = "Tester", Email = "tom@test.de", Role = "User", IsActive = true }
|
||||
};
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
private void ShowCreateModal()
|
||||
{
|
||||
currentUser = new AppUser { IsActive = true, Role = "User" };
|
||||
isEditMode = false;
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
private void ShowEditModal(AppUser user)
|
||||
{
|
||||
currentUser = new AppUser
|
||||
{
|
||||
Id = user.Id,
|
||||
UserName = user.UserName,
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
Email = user.Email,
|
||||
Role = user.Role,
|
||||
IsActive = user.IsActive,
|
||||
Password = "" // bleibt leer → wird beim Edit nicht überschrieben
|
||||
};
|
||||
isEditMode = true;
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
private void CloseModal()
|
||||
{
|
||||
showModal = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void OnBackdropClick(MouseEventArgs args)
|
||||
{
|
||||
CloseModal();
|
||||
}
|
||||
|
||||
private void OnModalKeyDown(KeyboardEventArgs args)
|
||||
{
|
||||
if (args.Key == "Escape")
|
||||
{
|
||||
CloseModal();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveUser()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(currentUser.UserName) || string.IsNullOrWhiteSpace(currentUser.Email) ||
|
||||
(!isEditMode && string.IsNullOrWhiteSpace(currentUser.Password)))
|
||||
{
|
||||
await JS.InvokeVoidAsync("showToast", "Bitte alle Pflichtfelder ausfüllen!", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEditMode)
|
||||
{
|
||||
var existing = users.First(x => x.Id == currentUser.Id);
|
||||
existing.UserName = currentUser.UserName;
|
||||
existing.FirstName = currentUser.FirstName;
|
||||
existing.LastName = currentUser.LastName;
|
||||
existing.Email = currentUser.Email;
|
||||
existing.Role = currentUser.Role;
|
||||
existing.IsActive = currentUser.IsActive;
|
||||
// Passwort wird nur geändert, wenn was eingetragen ist
|
||||
if (!string.IsNullOrWhiteSpace(currentUser.Password))
|
||||
existing.PasswordHash = currentUser.Password; // hier einfach als Klartext für Demozwecke
|
||||
}
|
||||
else
|
||||
{
|
||||
currentUser.Id = users.Max(x => x.Id) + 1;
|
||||
currentUser.PasswordHash = currentUser.Password; // Dummy-Hash
|
||||
users.Add(currentUser);
|
||||
}
|
||||
|
||||
CloseModal();
|
||||
await JS.InvokeVoidAsync("showToast", isEditMode ? "✅ Benutzer aktualisiert!" : "✅ Benutzer angelegt!");
|
||||
}
|
||||
|
||||
private async Task DeleteUser(AppUser user)
|
||||
{
|
||||
var confirmed = await JS.InvokeAsync<bool>("confirm", $"Soll der Benutzer '{user.UserName}' wirklich gelöscht werden?");
|
||||
if (confirmed)
|
||||
{
|
||||
users.Remove(user);
|
||||
await JS.InvokeVoidAsync("showToast", "🗑️ Benutzer gelöscht");
|
||||
}
|
||||
}
|
||||
|
||||
// Dummy-User-Klasse nur für diese Seite
|
||||
public class AppUser
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty; // nur im Modal
|
||||
public string? PasswordHash { get; set; } // wird später mal richtig
|
||||
public string Role { get; set; } = "User";
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
}
|
||||
99
FilterCair.Client/Pages/CustomerList.razor
Normal file
99
FilterCair.Client/Pages/CustomerList.razor
Normal file
@@ -0,0 +1,99 @@
|
||||
@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
|
||||
</h4>
|
||||
|
||||
<!-- 🔍 Suchfeld -->
|
||||
<div class="mb-4 text-center">
|
||||
<input type="text" class="form-control w-50 mx-auto shadow-sm"
|
||||
placeholder="🔍 Kunde oder Standort suchen..."
|
||||
@bind="searchText"
|
||||
@bind:event="oninput" />
|
||||
</div>
|
||||
|
||||
<!-- 🏭 Kundenliste -->
|
||||
<div class="row g-4 justify-content-center">
|
||||
@if (customers == null)
|
||||
{
|
||||
<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>
|
||||
}
|
||||
|
||||
@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>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<CustomerModel>? customers;
|
||||
private string searchText = "";
|
||||
|
||||
private IEnumerable<CustomerModel> FilteredCustomers =>
|
||||
string.IsNullOrWhiteSpace(searchText)
|
||||
? customers ?? new List<CustomerModel>()
|
||||
: customers!.Where(c =>
|
||||
c.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
|
||||
c.Standort.Contains(searchText, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
customers = await CustomerService.GetCustomersAsync();
|
||||
Console.WriteLine($"UI: {customers?.Count ?? 0} Kunden empfangen");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"UI Fehler: {ex.Message}");
|
||||
customers = new();
|
||||
}
|
||||
|
||||
// Force UI Update
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task SelectCustomer(CustomerModel c)
|
||||
{
|
||||
|
||||
await State.SetCustomerAsync(c.Name);
|
||||
|
||||
Nav.NavigateTo($"/stations/{c.Id}");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -17,23 +19,20 @@
|
||||
<DataAnnotationsValidator />
|
||||
<ValidationSummary />
|
||||
|
||||
<!-- === BESTEHENDE FELDER (unverändert) === -->
|
||||
<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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Zustand</label>
|
||||
<InputSelect class="form-select form-select-lg" @bind-Value="filter.Zustand">
|
||||
@@ -43,105 +42,125 @@
|
||||
<option>Defekt</option>
|
||||
</InputSelect>
|
||||
</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-md-6">
|
||||
<label class="form-label fw-semibold">Letzte Wartung</label>
|
||||
<InputDate class="form-control form-control-lg" @bind-Value="filter.LetzteWartung" />
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- === NEU: FOTO-BEREICH === -->
|
||||
<div class="mt-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="mb-0 text-primary">
|
||||
<i class="bi bi-camera me-2"></i> Fotos (@filter.Photos.Count)
|
||||
</h5>
|
||||
<button type="button" class="btn btn-outline-primary rounded-pill" @onclick="TakePhoto">
|
||||
<i class="bi bi-camera-fill me-1"></i> Foto aufnehmen
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<!-- 📸 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>
|
||||
|
||||
@if (imagePreviews.Count > 0)
|
||||
@if (filter.Photos.Any())
|
||||
{
|
||||
<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 class="row g-3">
|
||||
@foreach (var photo in filter.Photos)
|
||||
{
|
||||
<div class="col-6 col-md-4 col-lg-3">
|
||||
<div class="position-relative">
|
||||
<img src="@photo" class="img-fluid rounded shadow-sm" style="height:180px; object-fit:cover; width:100%;" />
|
||||
<button type="button" class="btn btn-danger btn-sm position-absolute top-0 end-0 m-2 rounded-circle"
|
||||
@onclick="() => RemovePhoto(photo)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-light text-center py-4">
|
||||
<i class="bi bi-image fs-1 text-muted"></i>
|
||||
<p class="mt-2 text-muted">Noch keine Fotos – tippe auf "Foto aufnehmen"</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="d-grid mt-4">
|
||||
<div class="d-grid mt-5">
|
||||
<button type="submit" class="btn btn-primary btn-lg rounded-pill shadow-sm">
|
||||
<i class="bi bi-save me-2"></i> Speichern
|
||||
</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
@if (showToast)
|
||||
{
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<div class="toast align-items-center text-bg-success border-0 show fade-in-toast">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
✅ Filterdaten erfolgreich gespeichert!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@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 FilterModel filter = new() { Photos = new List<string>() };
|
||||
|
||||
private async Task OnFilesSelected(InputFileChangeEventArgs e)
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
imagePreviews.Clear();
|
||||
var uri = NavManager.ToAbsoluteUri(NavManager.Uri);
|
||||
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
|
||||
|
||||
foreach (var file in e.GetMultipleFiles(3)) // max. 3 Bilder
|
||||
if (int.TryParse(query.Get("customerId"), out int customerId))
|
||||
filter.CustomerId = customerId;
|
||||
|
||||
if (int.TryParse(query.Get("stationId"), out int stationId))
|
||||
filter.StationId = stationId;
|
||||
|
||||
if (int.TryParse(query.Get("filterId"), out int filterId) && filterId > 0)
|
||||
{
|
||||
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}");
|
||||
var existing = await FilterService.GetFilterByIdAsync(filterId);
|
||||
if (existing != null)
|
||||
{
|
||||
filter = existing;
|
||||
// Sicherstellen, dass Photos existieren
|
||||
filter.Photos ??= new List<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TakePhoto()
|
||||
{
|
||||
try
|
||||
{
|
||||
var photoBase64 = await JS.InvokeAsync<string>("takePhoto");
|
||||
if (!string.IsNullOrEmpty(photoBase64))
|
||||
{
|
||||
filter.Photos.Add(photoBase64);
|
||||
StateHasChanged();
|
||||
await JS.InvokeVoidAsync("showToast", "Foto hinzugefügt!", "success");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await JS.InvokeVoidAsync("showToast", "Kamera-Fehler", "error");
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemovePhoto(string photo)
|
||||
{
|
||||
filter.Photos.Remove(photo);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task SaveForm()
|
||||
{
|
||||
Console.WriteLine($"Gespeichert: {System.Text.Json.JsonSerializer.Serialize(filter)}");
|
||||
await JS.InvokeVoidAsync("console.log", "Filter gespeichert:", filter);
|
||||
|
||||
showToast = true;
|
||||
StateHasChanged();
|
||||
|
||||
await Task.Delay(3000);
|
||||
showToast = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
[Inject] NavigationManager? NavManager { get; set; }
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var qrParam = NavManager?.ToAbsoluteUri(NavManager.Uri).Query;
|
||||
if (!string.IsNullOrEmpty(qrParam))
|
||||
bool success = await FilterService.SaveFilterAsync(filter);
|
||||
if (success)
|
||||
{
|
||||
var parts = System.Web.HttpUtility.ParseQueryString(qrParam);
|
||||
filter.FilterId = parts.Get("id");
|
||||
await JS.InvokeVoidAsync("showToast", "Filter + Fotos gespeichert!", "success");
|
||||
NavManager.NavigateTo($"/stations/{filter.CustomerId}/filters/{filter.StationId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
119
FilterCair.Client/Pages/Filters.razor
Normal file
119
FilterCair.Client/Pages/Filters.razor
Normal file
@@ -0,0 +1,119 @@
|
||||
@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
|
||||
{
|
||||
var editedFilters = filters.Where(f => f.LetzteWartung.HasValue).OrderByDescending(f => f.LetzteWartung).ToList();
|
||||
var pendingFilters = filters.Except(editedFilters).ToList();
|
||||
|
||||
<!-- Bearbeitete Filter -->
|
||||
@if (editedFilters.Any())
|
||||
{
|
||||
<h5 class="text-success mt-4">
|
||||
<i class="bi bi-check-circle me-1"></i> Bearbeitet (@editedFilters.Count)
|
||||
</h5>
|
||||
<div class="row g-3">
|
||||
@foreach (var f in editedFilters)
|
||||
{
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card border-0 shadow-sm filter-card h-100" @onclick="() => OpenFilterForm(f)">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-semibold text-success">
|
||||
<i class="bi bi-funnel me-1"></i> @f.Bezeichnung
|
||||
</h5>
|
||||
<p class="text-muted small"><strong>Zustand:</strong> @f.Zustand</p>
|
||||
<p class="text-muted small">
|
||||
<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-success btn-sm rounded-pill">
|
||||
<i class="bi bi-pencil"></i> Bearbeiten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Noch nicht bearbeitet -->
|
||||
@if (pendingFilters.Any())
|
||||
{
|
||||
<h5 class="text-danger mt-4">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i> Offen (@pendingFilters.Count)
|
||||
</h5>
|
||||
<div class="row g-3">
|
||||
@foreach (var f in pendingFilters)
|
||||
{
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card border-0 shadow-sm filter-card h-100" @onclick="() => OpenFilterForm(f)">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-semibold text-danger">
|
||||
<i class="bi bi-funnel me-1"></i> @f.Bezeichnung
|
||||
</h5>
|
||||
<p class="text-muted small"><strong>Zustand:</strong> @f.Zustand</p>
|
||||
<p class="text-muted small text-danger">
|
||||
<strong>Noch nicht bearbeitet</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer bg-light text-end">
|
||||
<button class="btn btn-outline-danger btn-sm rounded-pill">
|
||||
<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={filter.StationId}&customerId={customerId}");
|
||||
}
|
||||
}
|
||||
@@ -3,59 +3,51 @@
|
||||
@inject NavigationManager Nav
|
||||
|
||||
<PageTitle>FilterCair Dashboard</PageTitle>
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="text-center mb-5">
|
||||
<h2 class="fw-bold text-primary mb-2">
|
||||
<i class="bi bi-wind me-2"></i> Willkommen bei FilterCair
|
||||
</h2>
|
||||
<p class="text-secondary fs-5">FÜÜS - Filter Überprüf- und Überwachungssoftware</p>
|
||||
|
||||
<span class="badge rounded-pill px-3 py-2 mt-3 fs-6"
|
||||
style="background-color:@(isOnline ? "#198754" : "#dc3545")">
|
||||
@(isOnline ? "🟢 Online" : "🔴 Offline")
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard-Kacheln -->
|
||||
<!-- RESPONSIVES GRID MIT ICONS -->
|
||||
<div class="row g-4 justify-content-center">
|
||||
<!-- MEINE AUFGABEN -->
|
||||
<div class="col-12 col-sm-6 col-lg-4">
|
||||
<div class="card dashboard-tile h-100" @onclick="@(() => Nav.NavigateTo("/qrscanner"))">
|
||||
<div class="card-body d-flex flex-column justify-content-center align-items-center text-center">
|
||||
<i class="bi bi-qr-code-scan display-5 mb-3 text-primary"></i>
|
||||
<h5 class="fw-semibold mb-2">QR-Code scannen</h5>
|
||||
<p class="text-muted small mb-0">Starte den Scan und erfasse Filterdaten direkt vor Ort.</p>
|
||||
<div class="card dashboard-tile h-100 shadow-sm border-0 rounded-4"
|
||||
@onclick="@(() => Nav.NavigateTo("/tasks"))"
|
||||
style="cursor: pointer;">
|
||||
<div class="card-body d-flex flex-column justify-content-center align-items-center text-center p-4">
|
||||
<i class="bi bi-list-check display-5 mb-3 text-primary"></i>
|
||||
<h5 class="fw-semibold mb-2">Meine Aufgaben</h5>
|
||||
<p class="text-muted small mb-0">Übersicht aller offenen Wartungen und Filterprüfungen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KUNDENÜBERSICHT -->
|
||||
<div class="col-12 col-sm-6 col-lg-4">
|
||||
<div class="card dashboard-tile h-100" @onclick="@(() => Nav.NavigateTo("/filterform"))">
|
||||
<div class="card-body d-flex flex-column justify-content-center align-items-center text-center">
|
||||
<i class="bi bi-funnel display-5 mb-3 text-primary"></i>
|
||||
<h5 class="fw-semibold mb-2">Filter erfassen</h5>
|
||||
<p class="text-muted small mb-0">Erfasse technische Daten oder ergänze Wartungsnotizen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-lg-4">
|
||||
<div class="card dashboard-tile h-100" @onclick="@ShowOfflineData">
|
||||
<div class="card-body d-flex flex-column justify-content-center align-items-center text-center">
|
||||
<i class="bi bi-database-down display-5 mb-3 text-primary"></i>
|
||||
<h5 class="fw-semibold mb-2">Offline-Daten</h5>
|
||||
<p class="text-muted small mb-0">Zeige gespeicherte Daten, wenn du ohne Verbindung arbeitest.</p>
|
||||
<div class="card dashboard-tile h-100 shadow-sm border-0 rounded-4"
|
||||
@onclick="@(() => Nav.NavigateTo("/customers"))"
|
||||
style="cursor: pointer;">
|
||||
<div class="card-body d-flex flex-column justify-content-center align-items-center text-center p-4">
|
||||
<i class="bi bi-buildings display-5 mb-3 text-primary"></i>
|
||||
<h5 class="fw-semibold mb-2">Kundenübersicht</h5>
|
||||
<p class="text-muted small mb-0">Wähle den Kundenstandort aus, um Filterdaten zu erfassen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- GRID END -->
|
||||
|
||||
<!-- 📍 Standortanzeige -->
|
||||
<!-- STANDORTANZEIGE -->
|
||||
<div class="text-center mt-5 text-muted small">
|
||||
📍 Aktueller Standort: @currentLocation
|
||||
<i class="bi bi-geo-alt-fill me-1"></i> Aktueller Standort: @currentLocation
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@code {
|
||||
private bool isOnline = true;
|
||||
private string currentLocation = "unbekannt";
|
||||
|
||||
215
FilterCair.Client/Pages/Stations.razor
Normal file
215
FilterCair.Client/Pages/Stations.razor
Normal file
@@ -0,0 +1,215 @@
|
||||
@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>
|
||||
|
||||
@if (isPreloadingFilters)
|
||||
{
|
||||
<div class="alert alert-info d-flex align-items-center mt-3">
|
||||
<div class="spinner-border spinner-border-sm me-3" role="status"></div>
|
||||
<div>
|
||||
<strong>Offline-Modus wird vorbereitet…</strong><br>
|
||||
<small>Lade alle Filter dieses Kunden (@preloadedCount von @totalStations Stationen)</small>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- 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;
|
||||
|
||||
private bool isPreloadingFilters = false;
|
||||
private int preloadedCount = 0;
|
||||
private int totalStations = 0;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadStationsAndPreloadAllFilters();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await JS.InvokeVoidAsync("QRScanner.init", DotNetObjectReference.Create(this));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadStationsAndPreloadAllFilters()
|
||||
{
|
||||
isLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
// 1. Stationen laden
|
||||
stations = await StationService.GetStationsByCustomerAsync(customerId);
|
||||
totalStations = stations.Count;
|
||||
|
||||
if (stations.Count == 0)
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Sofort alle Filter aller Stationen vorladen (parallel!)
|
||||
isPreloadingFilters = true;
|
||||
preloadedCount = 0;
|
||||
StateHasChanged();
|
||||
|
||||
var preloadTasks = stations.Select(async station =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Lädt Filter + speichert automatisch im IndexedDB (durch deinen Cache-Mechanismus)
|
||||
await FilterService.GetFiltersByStationAsync(station.Id);
|
||||
preloadedCount++;
|
||||
StateHasChanged(); // Live-Update des Fortschritts
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Preload Fehler für Station {station.Id}: {ex.Message}");
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(preloadTasks);
|
||||
|
||||
await JS.InvokeVoidAsync("showToast",
|
||||
$"✅ {preloadedCount} Stationen + alle Filter offline verfügbar!",
|
||||
"success");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await JS.InvokeVoidAsync("showToast", "Teilweise offline verfügbar", "warning");
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
isPreloadingFilters = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 async Task OnQrDetected(string code)
|
||||
{
|
||||
scanResult = code;
|
||||
StateHasChanged();
|
||||
|
||||
// QR-Code bereinigen (falls Leerzeichen oder Zeilenumbrüche drin sind)
|
||||
string qrCode = code.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(qrCode))
|
||||
{
|
||||
await JS.InvokeVoidAsync("showToast", "Ungültiger QR-Code", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Suche in Offline-DB nach Filter mit diesem QR-Code
|
||||
var filter = await FilterService.GetFilterByQrCodeAsync(qrCode);
|
||||
|
||||
if (filter != null)
|
||||
{
|
||||
await JS.InvokeVoidAsync("showToast", $"Filter gefunden: {filter.Bezeichnung}", "success");
|
||||
|
||||
// Stoppe Kamera sofort
|
||||
await StopScan();
|
||||
|
||||
// Navigiere direkt zur Bearbeitung – mit allen nötigen IDs
|
||||
Nav.NavigateTo($"/filterform?filterId={filter.Id}&customerId={filter.CustomerId}&stationId={filter.StationId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
await JS.InvokeVoidAsync("showToast", $"QR-Code '{qrCode}' nicht in Offline-Daten gefunden", "warning");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await JS.InvokeVoidAsync("showToast", "Fehler beim Suchen des Filters", "error");
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
81
FilterCair.Client/Pages/Tasks.razor
Normal file
81
FilterCair.Client/Pages/Tasks.razor
Normal file
@@ -0,0 +1,81 @@
|
||||
@page "/tasks"
|
||||
@using FilterCair.Shared.Models
|
||||
|
||||
|
||||
<div class="container py-4 fade-in">
|
||||
<h4 class="mb-4 text-primary text-center">
|
||||
<i class="bi bi-list-check me-2"></i> Meine Aufgaben
|
||||
</h4>
|
||||
|
||||
<div class="row g-3">
|
||||
@if (tasks.Count == 0)
|
||||
{
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-emoji-neutral display-4 d-block mb-3"></i>
|
||||
Keine Aufgaben vorhanden.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var t in tasks)
|
||||
{
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card task-card shadow-sm border-0 rounded-4">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-semibold">@t.Title</h5>
|
||||
<p class="text-muted small mb-2">@t.Beschreibung</p>
|
||||
|
||||
<span class="badge bg-@(GetStatusColor(t.Status)) me-2">@t.Status</span>
|
||||
<span class="badge bg-secondary">@t.Prio</span>
|
||||
|
||||
@if (t.FaelligBis.HasValue)
|
||||
{
|
||||
<div class="text-muted small mt-2">
|
||||
<i class="bi bi-calendar-event me-1"></i>
|
||||
Fällig: @t.FaelligBis.Value.ToShortDateString()
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<button class="btn btn-sm btn-outline-primary" @onclick="() => ToggleStatus(t)">
|
||||
<i class="bi bi-check2-circle me-1"></i> Status ändern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<TaskModel> tasks = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
tasks = new()
|
||||
{
|
||||
new() { Id = 1, Title = "Filter prüfen Halle A", Beschreibung = "Sichtprüfung durchführen", ZugewiesenAn = "Marc Wieland", Status = "Offen", Prio = "Normal", FaelligBis = DateTime.Today.AddDays(1) },
|
||||
new() { Id = 2, Title = "Wartung Filter B2", Beschreibung = "Druck prüfen & Dokumentation", ZugewiesenAn = "Marc Wieland", Status = "In Arbeit", Prio = "Hoch" },
|
||||
new() { Id = 3, Title = "Filter austauschen", Beschreibung = "Alten Filter entfernen & neuen einsetzen", ZugewiesenAn = "Marc Wieland", Status = "Erledigt", Prio = "Normal", FaelligBis = DateTime.Today.AddDays(-2) }
|
||||
};
|
||||
}
|
||||
|
||||
private void ToggleStatus(TaskModel t)
|
||||
{
|
||||
t.Status = t.Status switch
|
||||
{
|
||||
"Offen" => "In Arbeit",
|
||||
"In Arbeit" => "Erledigt",
|
||||
_ => "Offen"
|
||||
};
|
||||
}
|
||||
|
||||
private string GetStatusColor(string status) => status switch
|
||||
{
|
||||
"Erledigt" => "success",
|
||||
"In Arbeit" => "warning",
|
||||
_ => "secondary"
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
using FilterCair.Client;
|
||||
using FilterCair.Client.Services;
|
||||
using FilterCair.Client.Services.API;
|
||||
using FilterCair.Client.Services.Local;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
@@ -21,4 +24,16 @@ builder.Services.AddMsalAuthentication(options =>
|
||||
"api://54b010c7-4a9a-4f2d-bea3-9faba3f12495/API.Access");
|
||||
});
|
||||
|
||||
//Services
|
||||
builder.Services.AddScoped<AppState>();
|
||||
|
||||
//API Services
|
||||
builder.Services.AddScoped<CustomerService>();
|
||||
builder.Services.AddScoped<StationService>();
|
||||
builder.Services.AddScoped<FilterService>();
|
||||
|
||||
builder.Services.AddScoped<CustomerLocalService>();
|
||||
builder.Services.AddScoped<StationLocalService>();
|
||||
builder.Services.AddScoped<FilterLocalService>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
155
FilterCair.Client/Services/API/CustomerService.cs
Normal file
155
FilterCair.Client/Services/API/CustomerService.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using FilterCair.Shared.Models;
|
||||
using FilterCair.Client.Services.Local;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace FilterCair.Client.Services.API
|
||||
{
|
||||
public class CustomerService
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly CustomerLocalService _local;
|
||||
private readonly IJSRuntime _js;
|
||||
private string BaseUrl => _config["Api:BaseUrl"] + "/api/Customer";
|
||||
|
||||
public CustomerService(
|
||||
HttpClient http,
|
||||
IConfiguration config,
|
||||
CustomerLocalService local,
|
||||
IJSRuntime js)
|
||||
{
|
||||
_http = http;
|
||||
_config = config;
|
||||
_local = local;
|
||||
_js = js;
|
||||
}
|
||||
|
||||
// Online-Status prüfen
|
||||
private async Task<bool> IsOnlineAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _js.InvokeAsync<bool>("navigator.onLine");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true; // Fallback: versuchen online
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<CustomerModel>> GetCustomersAsync()
|
||||
{
|
||||
bool isOnline = await IsOnlineAsync();
|
||||
|
||||
Console.WriteLine($"[CustomerService] Online: {isOnline}");
|
||||
|
||||
if (!isOnline)
|
||||
{
|
||||
Console.WriteLine("Offline → lade aus IndexedDB");
|
||||
var local = await _local.GetAllAsync();
|
||||
Console.WriteLine($"IndexedDB liefert: {local?.Count ?? 0} Kunden");
|
||||
return local ?? new();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _http.GetFromJsonAsync<List<CustomerModel>>(BaseUrl);
|
||||
Console.WriteLine($"API liefert: {result?.Count ?? 0} Kunden");
|
||||
|
||||
if (result != null && result.Count > 0)
|
||||
{
|
||||
await _local.SaveAllAsync(result);
|
||||
}
|
||||
return result ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"API-Fehler: {ex.Message}");
|
||||
Console.WriteLine("Fallback auf IndexedDB");
|
||||
var fallback = await _local.GetAllAsync();
|
||||
Console.WriteLine($"Fallback liefert: {fallback?.Count ?? 0} Kunden");
|
||||
return fallback ?? new();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> AddCustomerAsync(CustomerModel customer)
|
||||
{
|
||||
bool isOnline = await IsOnlineAsync();
|
||||
|
||||
if (isOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync(BaseUrl, customer);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
await _local.AddOrUpdateAsync(customer);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Add online fehlgeschlagen: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Offline: nur lokal speichern
|
||||
Console.WriteLine("Offline – Kunde nur lokal gespeichert");
|
||||
await _local.AddOrUpdateAsync(customer);
|
||||
return true; // "Erfolg" im Offline-Modus
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateCustomerAsync(CustomerModel customer)
|
||||
{
|
||||
bool isOnline = await IsOnlineAsync();
|
||||
|
||||
if (isOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _http.PutAsJsonAsync($"{BaseUrl}/{customer.Id}", customer);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
await _local.AddOrUpdateAsync(customer);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Update online fehlgeschlagen: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Offline – Update nur lokal");
|
||||
await _local.AddOrUpdateAsync(customer);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteCustomerAsync(int id)
|
||||
{
|
||||
bool isOnline = await IsOnlineAsync();
|
||||
|
||||
if (isOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _http.DeleteAsync($"{BaseUrl}/{id}");
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
await _local.DeleteAsync(id);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Delete online fehlgeschlagen: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Offline – Delete nur lokal");
|
||||
await _local.DeleteAsync(id);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
161
FilterCair.Client/Services/API/FilterService.cs
Normal file
161
FilterCair.Client/Services/API/FilterService.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using FilterCair.Client.Services.Local;
|
||||
using FilterCair.Shared.Models;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace FilterCair.Client.Services.API
|
||||
{
|
||||
public class FilterService
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly FilterLocalService _local;
|
||||
private readonly IJSRuntime _js;
|
||||
private string BaseUrl => _config["Api:BaseUrl"] + "/api/Filter";
|
||||
|
||||
public FilterService(HttpClient http, IConfiguration config, FilterLocalService local, IJSRuntime js)
|
||||
{
|
||||
_http = http; _config = config; _local = local; _js = js;
|
||||
}
|
||||
|
||||
private async Task<bool> IsOnlineAsync()
|
||||
{
|
||||
try { return await _js.InvokeAsync<bool>("navigator.onLine"); }
|
||||
catch { return true; }
|
||||
}
|
||||
|
||||
public async Task<List<FilterModel>> GetFiltersByStationAsync(int stationId)
|
||||
{
|
||||
bool isOnline = await IsOnlineAsync();
|
||||
|
||||
if (!isOnline)
|
||||
{
|
||||
Console.WriteLine($"Offline → Filter für Station {stationId} aus IndexedDB");
|
||||
return await _local.GetFiltersAsync(stationId);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var filters = await _http.GetFromJsonAsync<List<FilterModel>>($"{BaseUrl}/byStation/{stationId}");
|
||||
if (filters != null) await _local.SaveFiltersAsync(stationId, filters);
|
||||
return filters ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"API-Fehler → Fallback: {ex.Message}");
|
||||
return await _local.GetFiltersAsync(stationId);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SaveFilterAsync(FilterModel filter)
|
||||
{
|
||||
bool isOnline = await IsOnlineAsync();
|
||||
|
||||
if (isOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = filter.Id > 0
|
||||
? await _http.PutAsJsonAsync($"{BaseUrl}/{filter.Id}", filter)
|
||||
: await _http.PostAsJsonAsync(BaseUrl, filter);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = filter.Id > 0 ? filter : await response.Content.ReadFromJsonAsync<FilterModel>();
|
||||
if (result != null) await _local.SaveFiltersAsync(filter.StationId, new[] { result }.ToList());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Offline: lokal + Outbox
|
||||
await _local.SaveFiltersAsync(filter.StationId, new[] { filter }.ToList());
|
||||
await _local.EnqueueAsync(filter.Id > 0 ? "update" : "add", filter.Id, filter);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<FilterModel?> GetFilterByIdAsync(int id)
|
||||
{
|
||||
bool isOnline = await IsOnlineAsync();
|
||||
|
||||
if (!isOnline)
|
||||
{
|
||||
// Offline: aus IndexedDB laden
|
||||
var allFilters = await _local.GetFiltersAsync(0); // 0 = alle Stationen ignorieren
|
||||
return allFilters.FirstOrDefault(f => f.Id == id);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var filter = await _http.GetFromJsonAsync<FilterModel>($"{BaseUrl}/{id}");
|
||||
if (filter != null)
|
||||
{
|
||||
await _local.SaveFiltersAsync(filter.StationId, new[] { filter }.ToList());
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"API-Fehler bei GetFilterByIdAsync: {ex.Message}");
|
||||
// Fallback auf IndexedDB
|
||||
var localFilters = await _local.GetFiltersAsync(0);
|
||||
return localFilters.FirstOrDefault(f => f.Id == id);
|
||||
}
|
||||
}
|
||||
|
||||
// Füge diese Methode einfach in deine FilterService-Klasse ein:
|
||||
public async Task<FilterModel?> GetFilterByQrCodeAsync(string qrCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(qrCode))
|
||||
return null;
|
||||
|
||||
qrCode = qrCode.Trim();
|
||||
|
||||
// Immer zuerst offline suchen – das wollen wir beim QR-Scan ja genau!
|
||||
// Wir laden ALLE Filter aus der IndexedDB (stationId = 0 bedeutet "alle")
|
||||
var allFilters = await _local.GetFiltersAsync(0);
|
||||
|
||||
var found = allFilters.FirstOrDefault(f =>
|
||||
!string.IsNullOrEmpty(f.QRCode) &&
|
||||
f.QRCode.Trim().Equals(qrCode, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (found != null)
|
||||
{
|
||||
Console.WriteLine($"QR-Code '{qrCode}' gefunden → Filter ID {found.Id} ({found.Bezeichnung})");
|
||||
return found;
|
||||
}
|
||||
|
||||
// Optional: Falls online und nicht gefunden → nochmal beim Server nachfragen?
|
||||
// (Für den Techniker vor Ort meist nicht nötig – aber kann man später einbauen)
|
||||
bool isOnline = await IsOnlineAsync();
|
||||
if (isOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fromServer = await _http.GetFromJsonAsync<FilterModel>($"{BaseUrl}/byQr/{qrCode}");
|
||||
if (fromServer != null)
|
||||
{
|
||||
await _local.SaveFiltersAsync(fromServer.StationId, new[] { fromServer }.ToList());
|
||||
return fromServer;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignorieren – war nur ein Bonus-Versuch
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"QR-Code '{qrCode}' nicht gefunden");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
57
FilterCair.Client/Services/API/StationService.cs
Normal file
57
FilterCair.Client/Services/API/StationService.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using FilterCair.Client.Services.Local;
|
||||
using FilterCair.Shared.Models;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
|
||||
namespace FilterCair.Client.Services.API
|
||||
{
|
||||
public class StationService
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly StationLocalService _local;
|
||||
private readonly IJSRuntime _js;
|
||||
private string BaseUrl => _config["Api:BaseUrl"] + "/api/Station";
|
||||
|
||||
public StationService(HttpClient http, IConfiguration config, StationLocalService local, IJSRuntime js)
|
||||
{
|
||||
_http = http;
|
||||
_config = config;
|
||||
_local = local;
|
||||
_js = js;
|
||||
}
|
||||
|
||||
private async Task<bool> IsOnlineAsync()
|
||||
{
|
||||
try { return await _js.InvokeAsync<bool>("navigator.onLine"); }
|
||||
catch { return true; }
|
||||
}
|
||||
|
||||
public async Task<List<StationModel>> GetStationsByCustomerAsync(int customerId)
|
||||
{
|
||||
bool isOnline = await IsOnlineAsync();
|
||||
|
||||
if (!isOnline)
|
||||
{
|
||||
Console.WriteLine($"Offline → Stationen für Kunde {customerId} aus IndexedDB");
|
||||
return await _local.GetStationsAsync(customerId);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var stations = await _http.GetFromJsonAsync<List<StationModel>>($"{BaseUrl}/byCustomer/{customerId}");
|
||||
if (stations != null && stations.Count > 0)
|
||||
{
|
||||
await _local.SaveStationsAsync(customerId, stations);
|
||||
}
|
||||
return stations ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"API-Fehler → Fallback auf IndexedDB: {ex.Message}");
|
||||
return await _local.GetStationsAsync(customerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
FilterCair.Client/Services/AppState.cs
Normal file
51
FilterCair.Client/Services/AppState.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace FilterCair.Client.Services
|
||||
{
|
||||
public class AppState
|
||||
{
|
||||
private readonly IJSRuntime _js;
|
||||
public AppState(IJSRuntime js)
|
||||
{
|
||||
_js = js;
|
||||
}
|
||||
|
||||
public string? SelectedCustomer { get; private set; }
|
||||
|
||||
public bool IsOnline { get; private set; } = true;
|
||||
public event Action? OnChange;
|
||||
|
||||
private void NotifyStateChanged() => OnChange?.Invoke();
|
||||
|
||||
public async Task SetCustomerAsync(string customer)
|
||||
{
|
||||
SelectedCustomer = customer;
|
||||
await _js.InvokeVoidAsync("sessionStorage.setItem", "selectedCustomer", customer);
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
public async Task LoadFromSessionAsync()
|
||||
{
|
||||
SelectedCustomer = await _js.InvokeAsync<string?>("sessionStorage.getItem", "selectedCustomer");
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
public void SetOnlineStatus(bool isOnline)
|
||||
{
|
||||
IsOnline = isOnline;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
public async Task ClearAsync()
|
||||
{
|
||||
SelectedCustomer = null;
|
||||
await _js.InvokeVoidAsync("sessionStorage.removeItem", "selectedCustomer");
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
68
FilterCair.Client/Services/Local/CustomerLocalService.cs
Normal file
68
FilterCair.Client/Services/Local/CustomerLocalService.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using FilterCair.Shared.Models;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace FilterCair.Client.Services.Local
|
||||
{
|
||||
public class CustomerLocalService
|
||||
{
|
||||
private readonly IJSRuntime _js;
|
||||
|
||||
public CustomerLocalService(IJSRuntime js)
|
||||
{
|
||||
_js = js;
|
||||
}
|
||||
|
||||
public async Task SaveAllAsync(List<CustomerModel> customers)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(customers, JsonOptions);
|
||||
try
|
||||
{
|
||||
await _js.InvokeVoidAsync("filterDb.saveAll", json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"filterDb.saveAll fehlgeschlagen: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<CustomerModel>> GetAllAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine("Lese aus IndexedDB...");
|
||||
var json = await _js.InvokeAsync<string>("filterDb.getAll");
|
||||
Console.WriteLine($"JS liefert JSON: {json?.Substring(0, Math.Min(100, json.Length))}...");
|
||||
|
||||
if (string.IsNullOrEmpty(json) || json == "[]")
|
||||
return new();
|
||||
|
||||
var result = JsonSerializer.Deserialize<List<CustomerModel>>(json, JsonOptions);
|
||||
Console.WriteLine($"Deserialisiert: {result?.Count ?? 0} Kunden");
|
||||
return result ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"IndexedDB Fehler: {ex.Message}");
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddOrUpdateAsync(CustomerModel customer)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(customer, JsonOptions);
|
||||
await _js.InvokeVoidAsync("filterDb.put", json);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
await _js.InvokeVoidAsync("filterDb.delete", id);
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
}
|
||||
}
|
||||
54
FilterCair.Client/Services/Local/FilterLocalService.cs
Normal file
54
FilterCair.Client/Services/Local/FilterLocalService.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using FilterCair.Shared.Models;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace FilterCair.Client.Services.Local
|
||||
{
|
||||
// Services/Local/FilterLocalService.cs
|
||||
public class FilterLocalService
|
||||
{
|
||||
private readonly IJSRuntime _js;
|
||||
public FilterLocalService(IJSRuntime js) => _js = js;
|
||||
|
||||
public async Task SaveFiltersAsync(int stationId, List<FilterModel> filters)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(filters, JsonOptions);
|
||||
await _js.InvokeVoidAsync("filterDb.saveFilters", stationId, json);
|
||||
}
|
||||
|
||||
public async Task<List<FilterModel>> GetFiltersAsync(int stationId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (stationId == 0)
|
||||
{
|
||||
// Alle Filter laden
|
||||
var json = await _js.InvokeAsync<string>("filterDb.getAllFilters");
|
||||
return JsonSerializer.Deserialize<List<FilterModel>>(json, JsonOptions) ?? new();
|
||||
}
|
||||
else
|
||||
{
|
||||
var json = await _js.InvokeAsync<string>("filterDb.getFilters", stationId);
|
||||
return JsonSerializer.Deserialize<List<FilterModel>>(json, JsonOptions) ?? new();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"GetFiltersAsync Fehler: {ex.Message}");
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EnqueueAsync(string action, int entityId, FilterModel payload)
|
||||
{
|
||||
var payloadJson = JsonSerializer.Serialize(payload, JsonOptions);
|
||||
await _js.InvokeVoidAsync("filterDb.enqueue", action, entityId, payloadJson);
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
}
|
||||
}
|
||||
43
FilterCair.Client/Services/Local/StationLocalService.cs
Normal file
43
FilterCair.Client/Services/Local/StationLocalService.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
// Services/Local/StationLocalService.cs
|
||||
using FilterCair.Shared.Models;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace FilterCair.Client.Services.Local
|
||||
{
|
||||
public class StationLocalService
|
||||
{
|
||||
private readonly IJSRuntime _js;
|
||||
|
||||
public StationLocalService(IJSRuntime js)
|
||||
{
|
||||
_js = js;
|
||||
}
|
||||
|
||||
public async Task SaveStationsAsync(int customerId, List<StationModel> stations)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(stations, JsonOptions);
|
||||
await _js.InvokeVoidAsync("filterDb.saveStations", customerId, json);
|
||||
}
|
||||
|
||||
public async Task<List<StationModel>> GetStationsAsync(int customerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = await _js.InvokeAsync<string>("filterDb.getStations", customerId);
|
||||
return JsonSerializer.Deserialize<List<StationModel>>(json, JsonOptions) ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"StationLocalService Fehler: {ex.Message}");
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
}
|
||||
}
|
||||
BIN
FilterCair.Client/client.zip
Normal file
BIN
FilterCair.Client/client.zip
Normal file
Binary file not shown.
17
FilterCair.Client/package.json
Normal file
17
FilterCair.Client/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "filtercair.client",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
"jsqr": "^1.4.0"
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,6 @@
|
||||
]
|
||||
},
|
||||
"Api": {
|
||||
"BaseUrl": "https://filtercair-api.azurewebsites.net"
|
||||
"BaseUrl": "https://filtercair-server-gwctc2gbf5f4axgj.westeurope-01.azurewebsites.net"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +220,133 @@ code {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.image-preview img:hover {
|
||||
transform: scale(1.05);
|
||||
.image-preview img:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
|
||||
.task-card {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.task-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 16px rgba(0, 91, 153, 0.15);
|
||||
}
|
||||
|
||||
|
||||
.customer-card {
|
||||
border-radius: 1rem;
|
||||
transition: all 0.25s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.customer-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 18px rgba(0, 91, 153, 0.15);
|
||||
}
|
||||
|
||||
.customer-card:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
|
||||
.admin-tile {
|
||||
border-radius: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.admin-tile:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 20px rgba(0, 91, 153, 0.15);
|
||||
}
|
||||
|
||||
|
||||
.modal-dialog {
|
||||
max-width: 500px;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.modal-dialog {
|
||||
max-width: 90%;
|
||||
margin: 0.5rem auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.station-card {
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(0,0,0,0.05) !important;
|
||||
}
|
||||
|
||||
.station-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 12px 24px rgba(0,0,0,0.15) !important;
|
||||
border-color: #198754 !important;
|
||||
}
|
||||
|
||||
|
||||
/* app.css */
|
||||
.filter-card {
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(0,0,0,0.05) !important;
|
||||
}
|
||||
|
||||
.filter-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 12px 24px rgba(0,0,0,0.15) !important;
|
||||
border-color: #0d6efd !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.status-dot {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
animation: pulse 2s infinite;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.status-dot.online {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.status-dot.offline {
|
||||
color: #dc3545;
|
||||
animation: blink 1.5s infinite;
|
||||
}
|
||||
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
2105
FilterCair.Client/wwwroot/css/bootstrap-icons.css
vendored
Normal file
2105
FilterCair.Client/wwwroot/css/bootstrap-icons.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
5
FilterCair.Client/wwwroot/css/bootstrap-icons.min.css
vendored
Normal file
5
FilterCair.Client/wwwroot/css/bootstrap-icons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
12048
FilterCair.Client/wwwroot/css/bootstrap.css
vendored
Normal file
12048
FilterCair.Client/wwwroot/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
FilterCair.Client/wwwroot/css/bootstrap.css.map
Normal file
1
FilterCair.Client/wwwroot/css/bootstrap.css.map
Normal file
File diff suppressed because one or more lines are too long
6
FilterCair.Client/wwwroot/css/bootstrap.min.css
vendored
Normal file
6
FilterCair.Client/wwwroot/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
FilterCair.Client/wwwroot/css/bootstrap.min.css.map
Normal file
1
FilterCair.Client/wwwroot/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
BIN
FilterCair.Client/wwwroot/fonts/bootstrap-icons.woff
Normal file
BIN
FilterCair.Client/wwwroot/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
FilterCair.Client/wwwroot/fonts/bootstrap-icons.woff2
Normal file
BIN
FilterCair.Client/wwwroot/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
@@ -7,12 +7,12 @@
|
||||
<title>FilterCair</title>
|
||||
<base href="/" />
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap & Icons lokal-->
|
||||
<link rel="stylesheet" href="css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="css/bootstrap-icons.css" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
<link rel="stylesheet" href="FilterCair.Client.styles.css" />
|
||||
|
||||
<link href="FilterCair.Client.styles.css" rel="stylesheet" />
|
||||
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
@@ -39,12 +39,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js"></script>
|
||||
<!-- Lokale Scripte-->
|
||||
<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="https://unpkg.com/dexie@3.2.4/dist/dexie.min.js"></script>
|
||||
<script src="js/filterdb.js"></script>
|
||||
<script src="js/onlineStatus.js"></script>
|
||||
<script src="js/camera.js"></script>
|
||||
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
|
||||
|
||||
|
||||
6312
FilterCair.Client/wwwroot/js/bootstrap.bundle.js
vendored
Normal file
6312
FilterCair.Client/wwwroot/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
FilterCair.Client/wwwroot/js/bootstrap.bundle.js.map
Normal file
1
FilterCair.Client/wwwroot/js/bootstrap.bundle.js.map
Normal file
File diff suppressed because one or more lines are too long
7
FilterCair.Client/wwwroot/js/bootstrap.bundle.min.js
vendored
Normal file
7
FilterCair.Client/wwwroot/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
FilterCair.Client/wwwroot/js/bootstrap.bundle.min.js.map
Normal file
1
FilterCair.Client/wwwroot/js/bootstrap.bundle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
17
FilterCair.Client/wwwroot/js/camera.js
Normal file
17
FilterCair.Client/wwwroot/js/camera.js
Normal file
@@ -0,0 +1,17 @@
|
||||
window.takePhoto = async () => {
|
||||
const video = document.createElement('video');
|
||||
const canvas = document.createElement('canvas');
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } });
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
video.onloadedmetadata = () => {
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
canvas.getContext('2d').drawImage(video, 0, 0);
|
||||
stream.getTracks().forEach(t => t.stop());
|
||||
resolve(canvas.toDataURL('image/jpeg', 0.8));
|
||||
};
|
||||
});
|
||||
};
|
||||
2
FilterCair.Client/wwwroot/js/dexie.min.js
vendored
Normal file
2
FilterCair.Client/wwwroot/js/dexie.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
227
FilterCair.Client/wwwroot/js/filterdb.js
Normal file
227
FilterCair.Client/wwwroot/js/filterdb.js
Normal file
@@ -0,0 +1,227 @@
|
||||
// wwwroot/js/filterdb.js
|
||||
class FilterCairDB extends Dexie {
|
||||
constructor() {
|
||||
super('FilterOfflineDB');
|
||||
this.version(4).stores({
|
||||
customers: 'id, name, standort, email, ansprechpartner',
|
||||
stations: 'id, customerId, name, standort',
|
||||
filters: 'id, stationId, bezzeichnung, typ',
|
||||
outbox: '++id, action, entityId, payload, timestamp'
|
||||
});
|
||||
this.customers = this.table('customers');
|
||||
this.stations = this.table('stations');
|
||||
this.filters = this.table('filters');
|
||||
this.outbox = this.table('outbox');
|
||||
}
|
||||
}
|
||||
const db = new FilterCairDB();
|
||||
|
||||
window.filterDb = {
|
||||
saveAll: async (customersJson) => {
|
||||
try {
|
||||
const customers = JSON.parse(customersJson);
|
||||
await db.customers.clear();
|
||||
await db.customers.bulkPut(customers);
|
||||
console.log(`IndexedDB: ${customers.length} Kunden gespeichert`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('saveAll Fehler:', err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
getAll: async () => {
|
||||
try {
|
||||
const customers = await db.customers.toArray();
|
||||
console.log('IndexedDB Inhalt:', customers);
|
||||
return JSON.stringify(customers);
|
||||
} catch (err) {
|
||||
console.error('getAll Fehler:', err);
|
||||
return '[]';
|
||||
}
|
||||
},
|
||||
|
||||
put: async (customerJson) => {
|
||||
try {
|
||||
const customer = JSON.parse(customerJson);
|
||||
await db.customers.put(customer);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('put Fehler:', err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
delete: async (id) => {
|
||||
try {
|
||||
await db.customers.delete(id);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('delete Fehler:', err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
saveStations: async (customerId, stationsJson) => {
|
||||
try {
|
||||
const stations = JSON.parse(stationsJson);
|
||||
await db.stations.where('customerId').equals(customerId).delete();
|
||||
await db.stations.bulkPut(stations);
|
||||
console.log(`IndexedDB: ${stations.length} Stationen für Kunde ${customerId} gespeichert`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('saveStations Fehler:', err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
getStations: async (customerId) => {
|
||||
try {
|
||||
const stations = await db.stations.where('customerId').equals(customerId).toArray();
|
||||
return JSON.stringify(stations);
|
||||
} catch (err) {
|
||||
console.error('getStations Fehler:', err);
|
||||
return '[]';
|
||||
}
|
||||
},
|
||||
|
||||
saveFilters: async (stationId, filtersJson) => {
|
||||
try {
|
||||
const filters = JSON.parse(filtersJson);
|
||||
await db.filters.where('stationId').equals(stationId).delete();
|
||||
await db.filters.bulkPut(filters.map(f => ({
|
||||
...f,
|
||||
einbaudatum: f.einbaudatum ? new Date(f.einbaudatum) : null,
|
||||
letzteWartung: f.letzteWartung ? new Date(f.letzteWartung) : null
|
||||
})));
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('saveFilters Fehler:', err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
getFilters: async (stationId) => {
|
||||
try {
|
||||
const filters = await db.filters.where('stationId').equals(stationId).toArray();
|
||||
return JSON.stringify(filters);
|
||||
} catch (err) {
|
||||
console.error('getFilters Fehler:', err);
|
||||
return '[]';
|
||||
}
|
||||
},
|
||||
|
||||
getAllFilters: async () => {
|
||||
try {
|
||||
const filters = await db.filters.toArray();
|
||||
return JSON.stringify(filters);
|
||||
} catch (err) {
|
||||
console.error('getAllFilters Fehler:', err);
|
||||
return '[]';
|
||||
}
|
||||
},
|
||||
|
||||
enqueue: async (action, entityId, payload) => {
|
||||
try {
|
||||
const payloadJson = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
||||
await db.outbox.add({
|
||||
action,
|
||||
entityId: entityId || 0,
|
||||
payload: payloadJson,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
console.log(`Outbox: ${action} für ID ${entityId || 'neu'}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('enqueue Fehler:', err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
syncOutbox: async () => {
|
||||
if (!navigator.onLine) return false;
|
||||
|
||||
try {
|
||||
const pending = await db.outbox.orderBy('timestamp').toArray();
|
||||
if (pending.length === 0) return true;
|
||||
|
||||
console.log(`Sync: ${pending.length} Änderungen`);
|
||||
const baseUrl = 'https://filtercair-server-gwctc2gbf5f4axgj.westeurope-01.azurewebsites.net/api/Filter';
|
||||
|
||||
for (const item of pending) {
|
||||
let success = false;
|
||||
|
||||
try {
|
||||
if (item.action === 'add') {
|
||||
const res = await fetch(baseUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: item.payload
|
||||
});
|
||||
success = res.ok;
|
||||
if (success) {
|
||||
const newFilter = await res.json();
|
||||
await db.filters.put(newFilter);
|
||||
if (item.entityId === 0) {
|
||||
await db.outbox.delete(item.id);
|
||||
}
|
||||
}
|
||||
} else if (item.action === 'update') {
|
||||
const payload = JSON.parse(item.payload);
|
||||
const id = payload.id || item.entityId;
|
||||
if (!id) {
|
||||
console.error('Update ohne ID!', item);
|
||||
await db.outbox.delete(item.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
const res = await fetch(`${baseUrl}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: item.payload
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(`PUT ${id} fehlgeschlagen: ${res.status} ${res.statusText}`);
|
||||
continue; // Nicht löschen – später erneut
|
||||
}
|
||||
|
||||
success = true;
|
||||
await db.filters.put(payload);
|
||||
await db.outbox.delete(item.id);
|
||||
console.log(`Update erfolgreich: ID ${id}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Sync Fehler bei ${item.action}:`, err);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// Nur bei Erfolg löschen – bereits oben
|
||||
}
|
||||
}
|
||||
|
||||
// KEIN refreshAllFilters! API hat kein GET /api/Filter
|
||||
console.log('Sync abgeschlossen – keine globale Aktualisierung');
|
||||
return true;
|
||||
|
||||
} catch (err) {
|
||||
console.error('syncOutbox Fehler:', err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
getPendingCount: async () => {
|
||||
return await db.outbox.count();
|
||||
}
|
||||
};
|
||||
|
||||
// SIGNAL: filterDb ist vollständig geladen und bereit
|
||||
window.filterDbReady = true;
|
||||
console.log('filterDb bereit!');
|
||||
|
||||
// Online-Status für C# (falls benötigt)
|
||||
window.navigator = window.navigator || {};
|
||||
window.navigator.onLine = () => navigator.onLine;
|
||||
10102
FilterCair.Client/wwwroot/js/jsQR.js
Normal file
10102
FilterCair.Client/wwwroot/js/jsQR.js
Normal file
File diff suppressed because it is too large
Load Diff
2
FilterCair.Client/wwwroot/js/onlineStatus.js
Normal file
2
FilterCair.Client/wwwroot/js/onlineStatus.js
Normal file
@@ -0,0 +1,2 @@
|
||||
window.navigator = window.navigator || {};
|
||||
window.navigator.onLine = () => navigator.onLine;
|
||||
30
FilterCair.Client/wwwroot/js/toastcontainer.js
Normal file
30
FilterCair.Client/wwwroot/js/toastcontainer.js
Normal 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;
|
||||
}
|
||||
97
FilterCair.Server/Controllers/CustomerController.cs
Normal file
97
FilterCair.Server/Controllers/CustomerController.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using FilterCair.Server.Data;
|
||||
using FilterCair.Shared.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace FilterCair.Server.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
//[Authorize] //
|
||||
public class CustomerController : ControllerBase
|
||||
{
|
||||
private readonly FilterCairContext _context;
|
||||
|
||||
public CustomerController(FilterCairContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
//Load all Customers
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<CustomerModel>>> GetCustomers()
|
||||
{
|
||||
return await _context.Customers.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
//Load single Customer
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<CustomerModel>> GetCustomer(int id)
|
||||
{
|
||||
var customer = await _context.Customers.FindAsync(id);
|
||||
if (customer == null)
|
||||
return NotFound();
|
||||
|
||||
return customer;
|
||||
}
|
||||
|
||||
//Create Customer
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<CustomerModel>> PostCustomer(CustomerModel customer)
|
||||
{
|
||||
_context.Customers.Add(customer);
|
||||
await _context.SaveChangesAsync();
|
||||
return CreatedAtAction(nameof(GetCustomer), new { id = customer.Id }, customer);
|
||||
}
|
||||
|
||||
|
||||
//Delete Customer
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteCustomer(int id)
|
||||
{
|
||||
var customer = await _context.Customers.FindAsync(id);
|
||||
if (customer == null)
|
||||
return NotFound();
|
||||
|
||||
_context.Customers.Remove(customer);
|
||||
await _context.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
//PUT Customer
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> PutCustomer(int id, CustomerModel customer)
|
||||
{
|
||||
if (id != customer.Id)
|
||||
return BadRequest();
|
||||
|
||||
var existing = await _context.Customers.FindAsync(id);
|
||||
if (existing == null)
|
||||
return NotFound();
|
||||
|
||||
existing.Name = customer.Name;
|
||||
existing.Standort = customer.Standort;
|
||||
existing.Ansprechpartner = customer.Ansprechpartner;
|
||||
existing.Email = customer.Email;
|
||||
existing.Telefon = customer.Telefon;
|
||||
|
||||
_context.Entry(existing).State = EntityState.Modified;
|
||||
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
if (!_context.Customers.Any(c => c.Id == id))
|
||||
return NotFound();
|
||||
else
|
||||
throw;
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
70
FilterCair.Server/Controllers/FilterController.cs
Normal file
70
FilterCair.Server/Controllers/FilterController.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using FilterCair.Server.Data;
|
||||
using FilterCair.Shared.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace FilterCair.Server.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class FilterController : ControllerBase
|
||||
{
|
||||
private readonly FilterCairContext _context;
|
||||
|
||||
public FilterController(FilterCairContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
[HttpGet("byStation/{stationId}")]
|
||||
public async Task<ActionResult<IEnumerable<FilterModel>>> GetFiltersByStation(int stationId)
|
||||
{
|
||||
return await _context.Filters
|
||||
.Where(f => f.StationId == stationId).ToListAsync();
|
||||
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateFilter(FilterModel filter)
|
||||
{
|
||||
_context.Filters.Add(filter);
|
||||
await _context.SaveChangesAsync();
|
||||
return CreatedAtAction(nameof(GetFiltersByStation), new { stationId = filter.StationId }, filter);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<FilterModel>> GetFilterById(int id)
|
||||
{
|
||||
var filter = await _context.Filters.FindAsync(id);
|
||||
if (filter == null)
|
||||
return NotFound();
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> UpdateFilter(int id, FilterModel updatedFilter)
|
||||
{
|
||||
if (id != updatedFilter.Id)
|
||||
return BadRequest("ID stimmt nicht überein.");
|
||||
|
||||
var existing = await _context.Filters.FindAsync(id);
|
||||
if (existing == null)
|
||||
return NotFound();
|
||||
|
||||
// Werte übernehmen
|
||||
existing.Bezeichnung = updatedFilter.Bezeichnung;
|
||||
existing.Typ = updatedFilter.Typ;
|
||||
existing.Seriennummer = updatedFilter.Seriennummer;
|
||||
existing.Einbaudatum = updatedFilter.Einbaudatum;
|
||||
existing.LetzteWartung = updatedFilter.LetzteWartung;
|
||||
existing.Zustand = updatedFilter.Zustand;
|
||||
existing.QRCode = updatedFilter.QRCode;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
35
FilterCair.Server/Controllers/StationController.cs
Normal file
35
FilterCair.Server/Controllers/StationController.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using FilterCair.Server.Data;
|
||||
using FilterCair.Shared.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace FilterCair.Server.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class StationController : ControllerBase
|
||||
{
|
||||
private readonly FilterCairContext _context;
|
||||
|
||||
public StationController(FilterCairContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
[HttpGet("byCustomer/{customerId}")]
|
||||
public async Task<ActionResult<IEnumerable<StationModel>>> GetStationsByCustomer(int customerId)
|
||||
{
|
||||
return await _context.Stations
|
||||
.Where(s => s.CustomerId == customerId).ToListAsync();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateStation(StationModel station)
|
||||
{
|
||||
_context.Stations.Add(station);
|
||||
await _context.SaveChangesAsync();
|
||||
return CreatedAtAction(nameof(GetStationsByCustomer), new { customerId = station.CustomerId }, station);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
21
FilterCair.Server/Data/FilterCairContext.cs
Normal file
21
FilterCair.Server/Data/FilterCairContext.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using FilterCair.Shared.Models;
|
||||
|
||||
|
||||
namespace FilterCair.Server.Data
|
||||
{
|
||||
public class FilterCairContext : DbContext
|
||||
{
|
||||
public FilterCairContext(DbContextOptions<FilterCairContext> options)
|
||||
: base(options)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public DbSet<CustomerModel> Customers => Set<CustomerModel>();
|
||||
public DbSet<FilterModel> Filters => Set<FilterModel>();
|
||||
public DbSet<StationModel> Stations => Set<StationModel>();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
23
FilterCair.Server/Data/FilterCairContextFactory.cs
Normal file
23
FilterCair.Server/Data/FilterCairContextFactory.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.IO;
|
||||
|
||||
namespace FilterCair.Server.Data
|
||||
{
|
||||
public class FilterCairContextFactory : IDesignTimeDbContextFactory<FilterCairContext>
|
||||
{
|
||||
public FilterCairContext CreateDbContext(string[] args)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json")
|
||||
.Build();
|
||||
|
||||
var optionsBuilder = new DbContextOptionsBuilder<FilterCairContext>();
|
||||
optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection"));
|
||||
|
||||
return new FilterCairContext(optionsBuilder.Options);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
62
FilterCair.Server/Migrations/20251105122335_InitCustomers.Designer.cs
generated
Normal file
62
FilterCair.Server/Migrations/20251105122335_InitCustomers.Designer.cs
generated
Normal file
@@ -0,0 +1,62 @@
|
||||
// <auto-generated />
|
||||
using FilterCair.Server.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FilterCair.Server.Migrations
|
||||
{
|
||||
[DbContext(typeof(FilterCairContext))]
|
||||
[Migration("20251105122335_InitCustomers")]
|
||||
partial class InitCustomers
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FilterCair.Shared.Models.CustomerModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Ansprechpartner")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Standort")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Telefon")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Customers");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
38
FilterCair.Server/Migrations/20251105122335_InitCustomers.cs
Normal file
38
FilterCair.Server/Migrations/20251105122335_InitCustomers.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FilterCair.Server.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitCustomers : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Customers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Standort = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Ansprechpartner = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Email = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Telefon = table.Column<string>(type: "nvarchar(max)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Customers", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Customers");
|
||||
}
|
||||
}
|
||||
}
|
||||
133
FilterCair.Server/Migrations/20251105144410_AddStationsAndFilters.Designer.cs
generated
Normal file
133
FilterCair.Server/Migrations/20251105144410_AddStationsAndFilters.Designer.cs
generated
Normal file
@@ -0,0 +1,133 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FilterCair.Server.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FilterCair.Server.Migrations
|
||||
{
|
||||
[DbContext(typeof(FilterCairContext))]
|
||||
[Migration("20251105144410_AddStationsAndFilters")]
|
||||
partial class AddStationsAndFilters
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FilterCair.Shared.Models.CustomerModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Ansprechpartner")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Standort")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Telefon")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Customers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FilterCair.Shared.Models.FilterModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Bezeichnung")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("Einbaudatum")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("LetzteWartung")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("QRCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Seriennummer")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("StationId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Typ")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Zustand")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Filters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FilterCair.Shared.Models.StationModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Beschreibung")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("CustomerId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Standort")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stations");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FilterCair.Server.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddStationsAndFilters : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Filters",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
StationId = table.Column<int>(type: "int", nullable: false),
|
||||
Bezeichnung = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Typ = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Seriennummer = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Einbaudatum = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LetzteWartung = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
Zustand = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
QRCode = table.Column<string>(type: "nvarchar(max)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Filters", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Stations",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
CustomerId = table.Column<int>(type: "int", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Standort = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Beschreibung = table.Column<string>(type: "nvarchar(max)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Stations", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Filters");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Stations");
|
||||
}
|
||||
}
|
||||
}
|
||||
133
FilterCair.Server/Migrations/20251105144730_AddStationsAndFilters2.Designer.cs
generated
Normal file
133
FilterCair.Server/Migrations/20251105144730_AddStationsAndFilters2.Designer.cs
generated
Normal file
@@ -0,0 +1,133 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FilterCair.Server.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FilterCair.Server.Migrations
|
||||
{
|
||||
[DbContext(typeof(FilterCairContext))]
|
||||
[Migration("20251105144730_AddStationsAndFilters2")]
|
||||
partial class AddStationsAndFilters2
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FilterCair.Shared.Models.CustomerModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Ansprechpartner")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Standort")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Telefon")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Customers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FilterCair.Shared.Models.FilterModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Bezeichnung")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("Einbaudatum")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("LetzteWartung")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("QRCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Seriennummer")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("StationId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Typ")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Zustand")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Filters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FilterCair.Shared.Models.StationModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Beschreibung")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("CustomerId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Standort")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stations");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FilterCair.Server.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddStationsAndFilters2 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
130
FilterCair.Server/Migrations/FilterCairContextModelSnapshot.cs
Normal file
130
FilterCair.Server/Migrations/FilterCairContextModelSnapshot.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FilterCair.Server.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FilterCair.Server.Migrations
|
||||
{
|
||||
[DbContext(typeof(FilterCairContext))]
|
||||
partial class FilterCairContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FilterCair.Shared.Models.CustomerModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Ansprechpartner")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Standort")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Telefon")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Customers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FilterCair.Shared.Models.FilterModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Bezeichnung")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("Einbaudatum")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("LetzteWartung")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("QRCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Seriennummer")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("StationId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Typ")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Zustand")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Filters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FilterCair.Shared.Models.StationModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Beschreibung")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("CustomerId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Standort")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stations");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
FilterCair.Server/OnProf_Deploy.zip
Normal file
BIN
FilterCair.Server/OnProf_Deploy.zip
Normal file
Binary file not shown.
@@ -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,17 +8,25 @@ 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:5186", "http://localhost:5186")
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"metadata": {
|
||||
"_dependencyType": "compute.appService.windows"
|
||||
},
|
||||
"parameters": {
|
||||
"resourceGroupName": {
|
||||
"type": "string",
|
||||
"defaultValue": "FilterCair-RG",
|
||||
"metadata": {
|
||||
"description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
|
||||
}
|
||||
},
|
||||
"resourceGroupLocation": {
|
||||
"type": "string",
|
||||
"defaultValue": "westeurope",
|
||||
"metadata": {
|
||||
"description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support."
|
||||
}
|
||||
},
|
||||
"resourceName": {
|
||||
"type": "string",
|
||||
"defaultValue": "FilterCair-Server",
|
||||
"metadata": {
|
||||
"description": "Name of the main resource to be created by this template."
|
||||
}
|
||||
},
|
||||
"resourceLocation": {
|
||||
"type": "string",
|
||||
"defaultValue": "[parameters('resourceGroupLocation')]",
|
||||
"metadata": {
|
||||
"description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
|
||||
}
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
|
||||
"appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "Microsoft.Resources/resourceGroups",
|
||||
"name": "[parameters('resourceGroupName')]",
|
||||
"location": "[parameters('resourceGroupLocation')]",
|
||||
"apiVersion": "2019-10-01"
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Resources/deployments",
|
||||
"name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
|
||||
"resourceGroup": "[parameters('resourceGroupName')]",
|
||||
"apiVersion": "2019-10-01",
|
||||
"dependsOn": [
|
||||
"[parameters('resourceGroupName')]"
|
||||
],
|
||||
"properties": {
|
||||
"mode": "Incremental",
|
||||
"template": {
|
||||
"$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"resources": [
|
||||
{
|
||||
"location": "[parameters('resourceLocation')]",
|
||||
"name": "[parameters('resourceName')]",
|
||||
"type": "Microsoft.Web/sites",
|
||||
"apiVersion": "2015-08-01",
|
||||
"tags": {
|
||||
"[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[variables('appServicePlan_ResourceId')]"
|
||||
],
|
||||
"kind": "app",
|
||||
"properties": {
|
||||
"name": "[parameters('resourceName')]",
|
||||
"kind": "app",
|
||||
"httpsOnly": true,
|
||||
"reserved": false,
|
||||
"serverFarmId": "[variables('appServicePlan_ResourceId')]",
|
||||
"siteConfig": {
|
||||
"metadata": [
|
||||
{
|
||||
"name": "CURRENT_STACK",
|
||||
"value": "dotnet"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"type": "SystemAssigned"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": "[parameters('resourceLocation')]",
|
||||
"name": "[variables('appServicePlan_name')]",
|
||||
"type": "Microsoft.Web/serverFarms",
|
||||
"apiVersion": "2015-08-01",
|
||||
"sku": {
|
||||
"name": "S1",
|
||||
"tier": "Standard",
|
||||
"family": "S",
|
||||
"size": "S1"
|
||||
},
|
||||
"properties": {
|
||||
"name": "[variables('appServicePlan_name')]"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
@@ -6,4 +6,8 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
18
FilterCair.Shared/Models/CustomerModel.cs
Normal file
18
FilterCair.Shared/Models/CustomerModel.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FilterCair.Shared.Models
|
||||
{
|
||||
public class CustomerModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Standort { get; set; } = string.Empty;
|
||||
public string Ansprechpartner { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string Telefon { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,17 @@ 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 int CustomerId { 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;
|
||||
//Bilder
|
||||
public List<string> Photos { get; set; } = new();
|
||||
}
|
||||
}
|
||||
|
||||
17
FilterCair.Shared/Models/StationModel.cs
Normal file
17
FilterCair.Shared/Models/StationModel.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FilterCair.Shared.Models
|
||||
{
|
||||
public class StationModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int CustomerId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Standort { get; set; } = string.Empty;
|
||||
public string Beschreibung { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
21
FilterCair.Shared/Models/TaskModel.cs
Normal file
21
FilterCair.Shared/Models/TaskModel.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FilterCair.Shared.Models
|
||||
{
|
||||
public class TaskModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Beschreibung { get; set; } = string.Empty;
|
||||
public string ZugewiesenAn { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = "Offen";
|
||||
public string Prio { get; set; } = "Mittel";
|
||||
public DateTime? FaelligBis { get; set; }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user