Neuste fixes

This commit is contained in:
Marc Wieland 2025-11-06 21:45:46 +01:00
parent 371d8d9058
commit 73a8ec0e76
7 changed files with 228 additions and 90 deletions

View File

@ -3,13 +3,17 @@
@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">
@ -94,4 +98,63 @@
{
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();
");
}
}
}

View File

@ -25,39 +25,71 @@
}
else
{
<div class="row g-3 justify-content-center">
@foreach (var f in filters)
{
<div class="col-12 col-md-6 col-lg-4">
<div class="card border-0 shadow-sm filter-card h-100" @onclick="() => OpenFilterForm(f)">
<div class="card-body">
<h5 class="fw-semibold text-primary">
<i class="bi bi-funnel me-1"></i> @f.Bezeichnung
</h5>
<p class="text-muted small mb-1">
<strong>Typ:</strong> @f.Typ
</p>
<p class="text-muted small mb-1">
<strong>Seriennr.:</strong> @f.Seriennummer
</p>
<p class="text-muted small mb-1">
<strong>Zustand:</strong> @f.Zustand
</p>
<p class="text-muted small mb-0">
<strong>Letzte Wartung:</strong>
@(f.LetzteWartung?.ToString("dd.MM.yyyy") ?? "")
</p>
</div>
<div class="card-footer bg-light text-end">
<button class="btn btn-outline-primary btn-sm rounded-pill"
@onclick="() => OpenFilterForm(f)">
<i class="bi bi-pencil"></i> Bearbeiten
</button>
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>
}
</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>

View File

@ -11,10 +11,6 @@
</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 -->

View File

@ -73,7 +73,7 @@ namespace FilterCair.Client.Services.API
// Offline: lokal + Outbox
await _local.SaveFiltersAsync(filter.StationId, new[] { filter }.ToList());
await _local.EnqueueAsync(filter.Id > 0 ? "update" : "add", filter);
await _local.EnqueueAsync(filter.Id > 0 ? "update" : "add", filter.Id, filter);
return true;
}

View File

@ -39,10 +39,10 @@ namespace FilterCair.Client.Services.Local
}
}
public async Task EnqueueAsync(string action, FilterModel filter)
public async Task EnqueueAsync(string action, int entityId, FilterModel payload)
{
var payload = JsonSerializer.Serialize(filter, JsonOptions);
await _js.InvokeVoidAsync("filterDb.enqueue", action, filter.Id, payload);
var payloadJson = JsonSerializer.Serialize(payload, JsonOptions);
await _js.InvokeVoidAsync("filterDb.enqueue", action, entityId, payloadJson);
}
private static readonly JsonSerializerOptions JsonOptions = new()

View File

@ -300,4 +300,53 @@ code {
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);
}
}

View File

@ -1,21 +1,19 @@
// 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, entity, payload, timestamp'
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.filters = this.table('filters');
this.outbox = this.table('outbox');
}
}
const db = new FilterCairDB();
window.filterDb = {
@ -35,7 +33,7 @@ window.filterDb = {
getAll: async () => {
try {
const customers = await db.customers.toArray();
console.log('IndexedDB Inhalt:', customers);
console.log('IndexedDB Inhalt:', customers);
return JSON.stringify(customers);
} catch (err) {
console.error('getAll Fehler:', err);
@ -67,9 +65,7 @@ window.filterDb = {
saveStations: async (customerId, stationsJson) => {
try {
const stations = JSON.parse(stationsJson);
// Alte Stationen dieses Kunden löschen
await db.stations.where('customerId').equals(customerId).delete();
// Neue speichern
await db.stations.bulkPut(stations);
console.log(`IndexedDB: ${stations.length} Stationen für Kunde ${customerId} gespeichert`);
return true;
@ -79,7 +75,6 @@ window.filterDb = {
}
},
// NEU: Stationen eines Kunden lesen
getStations: async (customerId) => {
try {
const stations = await db.stations.where('customerId').equals(customerId).toArray();
@ -106,7 +101,6 @@ window.filterDb = {
}
},
// NEU: Filter einer Station lesen
getFilters: async (stationId) => {
try {
const filters = await db.filters.where('stationId').equals(stationId).toArray();
@ -127,16 +121,16 @@ window.filterDb = {
}
},
// NEU: Outbox Änderung speichern
enqueue: async (action, entityId, payload) => {
try {
const payloadJson = typeof payload === 'string' ? payload : JSON.stringify(payload);
await db.outbox.add({
action,
entityId,
payload: JSON.stringify(payload),
entityId: entityId || 0,
payload: payloadJson,
timestamp: Date.now()
});
console.log(`Outbox: ${action} für ID ${entityId}`);
console.log(`Outbox: ${action} für ID ${entityId || 'neu'}`);
return true;
} catch (err) {
console.error('enqueue Fehler:', err);
@ -144,7 +138,6 @@ window.filterDb = {
}
},
// NEU: Outbox syncen
syncOutbox: async () => {
if (!navigator.onLine) return false;
@ -153,14 +146,14 @@ window.filterDb = {
if (pending.length === 0) return true;
console.log(`Sync: ${pending.length} Änderungen`);
const baseUrl = '/api/Filter';
for (const item of pending) {
let success = false;
const url = '/api/Filter';
try {
if (item.action === 'add') {
const res = await fetch(url, {
const res = await fetch(baseUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: item.payload
@ -168,62 +161,67 @@ window.filterDb = {
success = res.ok;
if (success) {
const newFilter = await res.json();
await db.filters.put(newFilter); // ID aktualisieren
await db.filters.put(newFilter);
if (item.entityId === 0) {
await db.outbox.delete(item.id);
}
}
}
else if (item.action === 'update') {
} else if (item.action === 'update') {
const payload = JSON.parse(item.payload);
const res = await fetch(`${url}/${payload.id}`, {
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' },
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: item.payload
});
success = res.ok;
if (success) {
await db.filters.put(payload);
}
}
if (success) {
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
}
}
// Nach Sync: alle Filter neu laden
await window.filterDb.refreshAllFilters();
// 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;
}
},
// NEU: Alle Filter vom Server neu laden
refreshAllFilters: async () => {
try {
const res = await fetch('/api/Filter');
if (res.ok) {
const filters = await res.json();
await db.filters.clear();
await db.filters.bulkPut(filters);
console.log(`Refresh: ${filters.length} Filter aktualisiert`);
}
} catch (err) {
console.error('refreshAllFilters Fehler:', err);
}
},
// NEU: Anzahl offene Änderungen
getPendingCount: async () => {
return await db.outbox.count();
}
};
// Online-Status für C#
// 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;