Neuste fixes
This commit is contained in:
parent
371d8d9058
commit
73a8ec0e76
@ -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();
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
Loading…
Reference in New Issue
Block a user