Auftragsliste: lokale Scan-Tätigkeit nach oben + Geräte-Feld-Fundament
- Auftragsliste sortiert Aufträge mit lokalen Protokollen/Scans nach oben (lokale updatedAt), da Dolibarr-tms bei reiner App-Tätigkeit unverändert bleibt; Standard wieder "nur aktive Aufträge" - Datumszeile zeigt "zuletzt bearb." aus lokalem Protokoll, sonst Server-tms - Protokollzähler-Badge berücksichtigt auch lokale (noch nicht gesyncte) Protokolle - types.ts: neue optionale Felder für kommende Geräte-Features (Device: isFavorite/customName/openPorts/netbiosName/mdnsName/mdnsServices; neu SavedScan, MonitorEvent, DeviceMonitorSession; Protocol.savedScans/monitorSessions) - db.ts: normalizeProtocol ergänzt fehlende Arrays beim Laden alter Protokolle Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
53d91d1526
commit
2a75ad96b2
3 changed files with 108 additions and 16 deletions
|
|
@ -17,6 +17,20 @@ import type { Protocol } from './types';
|
|||
const DB_NAME = 'netdiag';
|
||||
const LS_PREFIX = 'netdiag.protocol.';
|
||||
|
||||
/**
|
||||
* Fehlende optionale Felder eines geladenen Protokolls defensiv ergänzen.
|
||||
* Ältere Protokolle (vor den Geräte-Features) haben weder `savedScans` noch
|
||||
* `monitorSessions` — ohne diese Normalisierung liefen Komponenten auf
|
||||
* `undefined.map`. Mutiert das Objekt und gibt es zurück.
|
||||
*/
|
||||
function normalizeProtocol(p: Protocol): Protocol {
|
||||
p.devices ??= [];
|
||||
p.measurements ??= [];
|
||||
p.savedScans ??= [];
|
||||
p.monitorSessions ??= [];
|
||||
return p;
|
||||
}
|
||||
|
||||
let useSqlite = false;
|
||||
let db: SQLiteDBConnection | null = null;
|
||||
|
||||
|
|
@ -66,10 +80,10 @@ export async function getProtocol(uuid: string): Promise<Protocol | null> {
|
|||
if (useSqlite && db) {
|
||||
const res = await db.query('SELECT json FROM protocols WHERE uuid = ?', [uuid]);
|
||||
const row = res.values?.[0];
|
||||
return row ? (JSON.parse(row.json) as Protocol) : null;
|
||||
return row ? normalizeProtocol(JSON.parse(row.json) as Protocol) : null;
|
||||
}
|
||||
const raw = localStorage.getItem(LS_PREFIX + uuid);
|
||||
return raw ? (JSON.parse(raw) as Protocol) : null;
|
||||
return raw ? normalizeProtocol(JSON.parse(raw) as Protocol) : null;
|
||||
}
|
||||
|
||||
/** Alle Protokolle laden (neueste zuerst) */
|
||||
|
|
@ -77,12 +91,12 @@ export async function getAllProtocols(): Promise<Protocol[]> {
|
|||
let list: Protocol[] = [];
|
||||
if (useSqlite && db) {
|
||||
const res = await db.query('SELECT json FROM protocols ORDER BY updated_at DESC');
|
||||
list = (res.values ?? []).map((r) => JSON.parse(r.json) as Protocol);
|
||||
list = (res.values ?? []).map((r) => normalizeProtocol(JSON.parse(r.json) as Protocol));
|
||||
} else {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key?.startsWith(LS_PREFIX)) {
|
||||
list.push(JSON.parse(localStorage.getItem(key)!) as Protocol);
|
||||
list.push(normalizeProtocol(JSON.parse(localStorage.getItem(key)!) as Protocol));
|
||||
}
|
||||
}
|
||||
list.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,54 @@ export interface Device {
|
|||
vendor?: string;
|
||||
deviceType?: string;
|
||||
note?: string;
|
||||
/** vom Benutzer als Favorit markiert (bleibt auch ohne gespeicherten Scan erhalten) */
|
||||
isFavorite?: boolean;
|
||||
/** benutzervergebener Name, überschreibt hostname in der Anzeige */
|
||||
customName?: string;
|
||||
/** Unix-Zeit der letzten Sichtung im Netz */
|
||||
lastSeen?: number;
|
||||
/** zuletzt beim Port-Scan gefundene offene Ports */
|
||||
openPorts?: number[];
|
||||
/** NetBIOS-Name (UDP-137-Abfrage) */
|
||||
netbiosName?: string;
|
||||
/** mDNS/Bonjour-Name */
|
||||
mdnsName?: string;
|
||||
/** angebotene mDNS-Dienste, z.B. ['_printer._tcp', '_googlecast._tcp'] */
|
||||
mdnsServices?: string[];
|
||||
}
|
||||
|
||||
/** Eingefrorener Snapshot eines IP-Scans (wieder aufrufbar) */
|
||||
export interface SavedScan {
|
||||
id: string;
|
||||
name: string;
|
||||
/** Unix-Zeit der Speicherung */
|
||||
createdAt: number;
|
||||
subnet: string;
|
||||
devices: Device[];
|
||||
}
|
||||
|
||||
/** Ein Ereignis der Geräte-Überwachung */
|
||||
export interface MonitorEvent {
|
||||
/** Unix-Zeit des Ereignisses */
|
||||
ts: number;
|
||||
ip: string;
|
||||
type: 'down' | 'up';
|
||||
/** Dauer des vorangegangenen Ausfalls in Sekunden (nur bei 'up') */
|
||||
durationSec?: number;
|
||||
}
|
||||
|
||||
/** Überwachungs-Sitzung des Geräte-Monitors (Kamera-Problem) */
|
||||
export interface DeviceMonitorSession {
|
||||
id: string;
|
||||
name: string;
|
||||
startedAt: number;
|
||||
endedAt?: number;
|
||||
/** Prüfintervall in Sekunden */
|
||||
intervalSec: number;
|
||||
/** überwachte Geräte */
|
||||
targets: { ip: string; label: string }[];
|
||||
events: MonitorEvent[];
|
||||
status: 'running' | 'stopped';
|
||||
}
|
||||
|
||||
/** Ampel-Bewertung einer Messung */
|
||||
|
|
@ -92,6 +140,10 @@ export interface Protocol {
|
|||
note: string;
|
||||
devices: Device[];
|
||||
measurements: Measurement[];
|
||||
/** gespeicherte IP-Scan-Snapshots (nur lokal, wird nicht synchronisiert) */
|
||||
savedScans?: SavedScan[];
|
||||
/** Geräte-Überwachungs-Sitzungen (nur lokal, wird nicht synchronisiert) */
|
||||
monitorSessions?: DeviceMonitorSession[];
|
||||
/** true solange noch nicht zum Server synchronisiert */
|
||||
dirty: boolean;
|
||||
updatedAt: number;
|
||||
|
|
|
|||
|
|
@ -12,12 +12,15 @@
|
|||
|
||||
let orders = $state<Order[]>([]);
|
||||
let search = $state('');
|
||||
// Standard: alle Aufträge, zuletzt bearbeitete zuerst (Server sortiert nach tms).
|
||||
// Haken setzen blendet abgeschlossene aus und zeigt nur aktive Aufträge.
|
||||
let onlyActive = $state(false);
|
||||
// Standard: nur aktive Aufträge. Haken entfernen zeigt auch abgeschlossene.
|
||||
// Sortierung: Aufträge mit lokaler Scan-Tätigkeit zuerst, dann Server-Reihenfolge (tms).
|
||||
let onlyActive = $state(true);
|
||||
let loading = $state(false);
|
||||
let loadError = $state('');
|
||||
|
||||
/** orderId → letzte lokale Protokoll-Bearbeitung + Anzahl (für Sortierung/Anzeige) */
|
||||
let localActivity = $state(new Map<number, { updatedAt: number; count: number }>());
|
||||
|
||||
let searchTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
/** Kunde + Ort als eine Zeile — das, was man im Kopf hat */
|
||||
|
|
@ -43,7 +46,23 @@
|
|||
loadError = '';
|
||||
try {
|
||||
const res = await listOrders({ open: onlyActive, q: search.trim() || undefined });
|
||||
orders = res.orders;
|
||||
// Lokale Protokoll-Tätigkeit erfassen — ein Scan auf dem Handy ändert die
|
||||
// Dolibarr-tms NICHT, daher hier clientseitig nach oben sortieren.
|
||||
const activity = new Map<number, { updatedAt: number; count: number }>();
|
||||
for (const p of await getAllProtocols()) {
|
||||
if (p.orderId == null) continue;
|
||||
const cur = activity.get(p.orderId);
|
||||
activity.set(p.orderId, {
|
||||
updatedAt: Math.max(cur?.updatedAt ?? 0, p.updatedAt),
|
||||
count: (cur?.count ?? 0) + 1,
|
||||
});
|
||||
}
|
||||
localActivity = activity;
|
||||
// Aufträge mit lokaler Tätigkeit zuerst (jüngste oben), Rest in
|
||||
// Server-Reihenfolge (tms DESC) — Array.sort ist stabil.
|
||||
orders = res.orders
|
||||
.slice()
|
||||
.sort((a, b) => (activity.get(b.id)?.updatedAt ?? 0) - (activity.get(a.id)?.updatedAt ?? 0));
|
||||
} catch (e) {
|
||||
loadError = e instanceof ApiError ? e.message : 'Laden fehlgeschlagen';
|
||||
} finally {
|
||||
|
|
@ -51,6 +70,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
/** Datums-Zusatz für die Auftragszeile: lokale Bearbeitung bevorzugt */
|
||||
function editedInfo(order: Order): string {
|
||||
const local = localActivity.get(order.id);
|
||||
if (local) return ' · zuletzt bearb. ' + fmtDate(local.updatedAt);
|
||||
if (order.tms) return ' · bearb. ' + fmtDate(order.tms);
|
||||
if (order.date) return ' · ' + fmtDate(order.date);
|
||||
return '';
|
||||
}
|
||||
|
||||
function onSearchInput() {
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(load, 300);
|
||||
|
|
@ -84,7 +112,8 @@
|
|||
}
|
||||
|
||||
onMount(async () => {
|
||||
onlyActive = (await Preferences.get({ key: 'nd_only_active' })).value === '1';
|
||||
const pref = (await Preferences.get({ key: 'nd_only_active' })).value;
|
||||
onlyActive = pref == null ? true : pref === '1';
|
||||
load();
|
||||
});
|
||||
</script>
|
||||
|
|
@ -121,6 +150,7 @@
|
|||
<p class="p-6 text-center text-sm text-zinc-500">Keine Aufträge gefunden.</p>
|
||||
{:else}
|
||||
{#each orders as order (order.id)}
|
||||
{@const pc = Math.max(order.protocolCount ?? 0, localActivity.get(order.id)?.count ?? 0)}
|
||||
<button
|
||||
class="flex w-full items-start gap-3 border-b border-zinc-800/60 px-3 py-3 text-left active:bg-zinc-800"
|
||||
onclick={() => openOrder(order)}
|
||||
|
|
@ -145,16 +175,12 @@
|
|||
{/if}
|
||||
<!-- Auftragsnummer + Bearbeitungsdatum: nur Kleingedrucktes -->
|
||||
<div class="truncate text-[11px] text-zinc-500">
|
||||
{order.ref}{order.refClient ? ' · ' + order.refClient : ''}{order.tms
|
||||
? ' · bearb. ' + fmtDate(order.tms)
|
||||
: order.date
|
||||
? ' · ' + fmtDate(order.date)
|
||||
: ''}
|
||||
{order.ref}{order.refClient ? ' · ' + order.refClient : ''}{editedInfo(order)}
|
||||
</div>
|
||||
</div>
|
||||
{#if order.protocolCount && order.protocolCount > 0}
|
||||
{#if pc > 0}
|
||||
<span class="flex shrink-0 items-center gap-1 text-xs text-sky-400">
|
||||
<FileStack size={14} />{order.protocolCount}
|
||||
<FileStack size={14} />{pc}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
|
|
|||
Loading…
Reference in a new issue