Auftragsliste nach Kunde+Ort, IP-Scanner mit Adapter-Erkennung
All checks were successful
Build APK / build-apk (push) Has been skipped

Auftragsliste: Kundenname + Adresse sind jetzt die Überschrift, die
Auftragsnummer nur noch Kleingedrucktes — Aufträge sind so ohne
Nummer-im-Kopf wiederzufinden. Auftragsnotiz wird mit angezeigt.

IP-Scanner: ist kein Netzbereich angegeben, nutzt das Tool den im
Protokoll hinterlegten; ist auch der leer, fragt es den aktiven
WLAN-/LAN-Adapter ab und scannt dessen Subnetz. Der ermittelte
Netzbereich wird ins Protokoll übernommen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-05-19 12:57:19 +02:00
parent 26eb418c21
commit 27dae2ce50
3 changed files with 79 additions and 13 deletions

View file

@ -1,6 +1,11 @@
/**
* Tool: IP-Scanner findet Geräte im Subnetz.
* Die gefundenen Geräte werden ins Protokoll übernommen.
*
* Netzbereich-Logik: ist im Dialog nichts angegeben, wird der im
* Protokoll hinterlegte Netzbereich genutzt; ist auch der leer,
* fragt das Tool den aktiven WLAN-/LAN-Adapter ab und scannt
* dessen Subnetz direkt.
*/
import { scanner } from '../../scanner';
@ -16,17 +21,48 @@ export const ipScanTool: Tool = {
params: [
{
key: 'subnet',
label: 'Netzbereich (CIDR)',
label: 'Netzbereich (CIDR) — leer = aktiver Adapter',
type: 'text',
placeholder: '192.168.1.0/24',
placeholder: 'leer lassen → automatisch über WLAN/LAN',
},
],
async run(ctx) {
const subnet = String(ctx.params.subnet || ctx.protocol.subnet || '192.168.1.0/24');
// 1. Dialog-Eingabe 2. im Protokoll hinterlegter Netzbereich
let subnet = String(ctx.params.subnet ?? '').trim() || String(ctx.protocol.subnet ?? '').trim();
let source: 'eingabe' | 'protokoll' | 'adapter' = subnet
? String(ctx.params.subnet ?? '').trim()
? 'eingabe'
: 'protokoll'
: 'adapter';
// 3. Nichts angegeben → aktiven Adapter abfragen und dessen Subnetz scannen
if (!subnet) {
try {
const local = await scanner.getLocalSubnet();
subnet = String(local.subnet ?? '').trim();
} catch {
/* kein Adapter ermittelbar — unten abgefangen */
}
}
if (!subnet) {
return {
label: 'Kein Netzbereich — WLAN/LAN nicht aktiv?',
result: { error: 'Netzbereich konnte nicht ermittelt werden' },
measureStatus: 2,
};
}
// Ermittelten Netzbereich ins Protokoll übernehmen, wenn dort noch leer
if (!String(ctx.protocol.subnet ?? '').trim()) {
ctx.protocol.subnet = subnet;
}
const { devices } = await scanner.ipScan({ subnet });
const via = source === 'adapter' ? ' (Adapter erkannt)' : '';
return {
label: `${devices.length} Geräte im Netz ${subnet}`,
result: { subnet, count: devices.length },
label: `${devices.length} Geräte im Netz ${subnet}${via}`,
result: { subnet, count: devices.length, quelle: source },
measureStatus: devices.length > 0 ? 0 : 1,
devices: devices.map((d) => ({
ip: d.ip,

View file

@ -34,6 +34,8 @@ export interface Order {
status: number;
/** true wenn Status 0/1/2 (aktiver Auftrag) */
open: boolean;
/** Öffentliche Auftragsnotiz — oft die eigentliche Beschreibung */
note?: string;
protocolCount?: number;
customer?: Partial<Customer>;
}

View file

@ -18,6 +18,24 @@
let searchTimer: ReturnType<typeof setTimeout>;
/** Kunde + Ort als eine Zeile — das, was man im Kopf hat */
function orderLocation(order: Order): string {
const c = order.customer;
if (!c) return '';
const ort = [c.zip, c.town].filter(Boolean).join(' ');
return [c.address, ort].filter(Boolean).join(', ');
}
/** Datum kurz (akzeptiert Sekunden- wie Millisekunden-Timestamp) */
function fmtDate(ts: number): string {
const ms = ts < 1e11 ? ts * 1000 : ts;
return new Date(ms).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: '2-digit',
});
}
async function load() {
loading = true;
loadError = '';
@ -102,26 +120,36 @@
{:else}
{#each orders as order (order.id)}
<button
class="flex w-full items-center 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)}
>
<div class="min-w-0 flex-1">
<!-- Kunde = Überschrift, das was man wiedererkennt -->
<div class="flex items-center gap-2">
<span class="font-medium">{order.ref}</span>
<span class="truncate font-medium">{order.customer?.name || order.ref}</span>
{#if !order.open}
<span class="rounded bg-zinc-700 px-1.5 py-0.5 text-[10px] text-zinc-300"
<span class="shrink-0 rounded bg-zinc-700 px-1.5 py-0.5 text-[10px] text-zinc-300"
>abgeschlossen</span
>
{/if}
</div>
<div class="truncate text-sm text-zinc-400">{order.customer?.name ?? ''}</div>
<div class="truncate text-xs text-zinc-500">
{order.customer?.zip ?? ''}
{order.customer?.town ?? ''}
<!-- Ort/Adresse -->
{#if orderLocation(order)}
<div class="truncate text-sm text-zinc-400">{orderLocation(order)}</div>
{/if}
<!-- Auftragsnotiz, falls vorhanden -->
{#if order.note}
<div class="truncate text-xs text-zinc-400">{order.note}</div>
{/if}
<!-- Auftragsnummer + Datum: nur Kleingedrucktes -->
<div class="truncate text-[11px] text-zinc-500">
{order.ref}{order.refClient ? ' · ' + order.refClient : ''}{order.date
? ' · ' + fmtDate(order.date)
: ''}
</div>
</div>
{#if order.protocolCount && order.protocolCount > 0}
<span class="flex 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}
</span>
{/if}