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 DB_NAME = 'netdiag';
|
||||||
const LS_PREFIX = 'netdiag.protocol.';
|
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 useSqlite = false;
|
||||||
let db: SQLiteDBConnection | null = null;
|
let db: SQLiteDBConnection | null = null;
|
||||||
|
|
||||||
|
|
@ -66,10 +80,10 @@ export async function getProtocol(uuid: string): Promise<Protocol | null> {
|
||||||
if (useSqlite && db) {
|
if (useSqlite && db) {
|
||||||
const res = await db.query('SELECT json FROM protocols WHERE uuid = ?', [uuid]);
|
const res = await db.query('SELECT json FROM protocols WHERE uuid = ?', [uuid]);
|
||||||
const row = res.values?.[0];
|
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);
|
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) */
|
/** Alle Protokolle laden (neueste zuerst) */
|
||||||
|
|
@ -77,12 +91,12 @@ export async function getAllProtocols(): Promise<Protocol[]> {
|
||||||
let list: Protocol[] = [];
|
let list: Protocol[] = [];
|
||||||
if (useSqlite && db) {
|
if (useSqlite && db) {
|
||||||
const res = await db.query('SELECT json FROM protocols ORDER BY updated_at DESC');
|
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 {
|
} else {
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
const key = localStorage.key(i);
|
const key = localStorage.key(i);
|
||||||
if (key?.startsWith(LS_PREFIX)) {
|
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);
|
list.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,54 @@ export interface Device {
|
||||||
vendor?: string;
|
vendor?: string;
|
||||||
deviceType?: string;
|
deviceType?: string;
|
||||||
note?: 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 */
|
/** Ampel-Bewertung einer Messung */
|
||||||
|
|
@ -92,6 +140,10 @@ export interface Protocol {
|
||||||
note: string;
|
note: string;
|
||||||
devices: Device[];
|
devices: Device[];
|
||||||
measurements: Measurement[];
|
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 */
|
/** true solange noch nicht zum Server synchronisiert */
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,15 @@
|
||||||
|
|
||||||
let orders = $state<Order[]>([]);
|
let orders = $state<Order[]>([]);
|
||||||
let search = $state('');
|
let search = $state('');
|
||||||
// Standard: alle Aufträge, zuletzt bearbeitete zuerst (Server sortiert nach tms).
|
// Standard: nur aktive Aufträge. Haken entfernen zeigt auch abgeschlossene.
|
||||||
// Haken setzen blendet abgeschlossene aus und zeigt nur aktive Aufträge.
|
// Sortierung: Aufträge mit lokaler Scan-Tätigkeit zuerst, dann Server-Reihenfolge (tms).
|
||||||
let onlyActive = $state(false);
|
let onlyActive = $state(true);
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let loadError = $state('');
|
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>;
|
let searchTimer: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
/** Kunde + Ort als eine Zeile — das, was man im Kopf hat */
|
/** Kunde + Ort als eine Zeile — das, was man im Kopf hat */
|
||||||
|
|
@ -43,7 +46,23 @@
|
||||||
loadError = '';
|
loadError = '';
|
||||||
try {
|
try {
|
||||||
const res = await listOrders({ open: onlyActive, q: search.trim() || undefined });
|
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) {
|
} catch (e) {
|
||||||
loadError = e instanceof ApiError ? e.message : 'Laden fehlgeschlagen';
|
loadError = e instanceof ApiError ? e.message : 'Laden fehlgeschlagen';
|
||||||
} finally {
|
} 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() {
|
function onSearchInput() {
|
||||||
clearTimeout(searchTimer);
|
clearTimeout(searchTimer);
|
||||||
searchTimer = setTimeout(load, 300);
|
searchTimer = setTimeout(load, 300);
|
||||||
|
|
@ -84,7 +112,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
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();
|
load();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -121,6 +150,7 @@
|
||||||
<p class="p-6 text-center text-sm text-zinc-500">Keine Aufträge gefunden.</p>
|
<p class="p-6 text-center text-sm text-zinc-500">Keine Aufträge gefunden.</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#each orders as order (order.id)}
|
{#each orders as order (order.id)}
|
||||||
|
{@const pc = Math.max(order.protocolCount ?? 0, localActivity.get(order.id)?.count ?? 0)}
|
||||||
<button
|
<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"
|
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)}
|
onclick={() => openOrder(order)}
|
||||||
|
|
@ -145,16 +175,12 @@
|
||||||
{/if}
|
{/if}
|
||||||
<!-- Auftragsnummer + Bearbeitungsdatum: nur Kleingedrucktes -->
|
<!-- Auftragsnummer + Bearbeitungsdatum: nur Kleingedrucktes -->
|
||||||
<div class="truncate text-[11px] text-zinc-500">
|
<div class="truncate text-[11px] text-zinc-500">
|
||||||
{order.ref}{order.refClient ? ' · ' + order.refClient : ''}{order.tms
|
{order.ref}{order.refClient ? ' · ' + order.refClient : ''}{editedInfo(order)}
|
||||||
? ' · bearb. ' + fmtDate(order.tms)
|
|
||||||
: order.date
|
|
||||||
? ' · ' + fmtDate(order.date)
|
|
||||||
: ''}
|
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<span class="flex shrink-0 items-center gap-1 text-xs text-sky-400">
|
||||||
<FileStack size={14} />{order.protocolCount}
|
<FileStack size={14} />{pc}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue