From 98bce1b0da675715b2e5482c0ad61ba21135f62f Mon Sep 17 00:00:00 2001
From: Server-Bibliothek <server-bibliothek@lokal>
Date: Di, 04 Aug 2026 07:00:04 +0200
Subject: [PATCH] Add a desktop client that knows what is installed locally

---
 desktop/frontend/src/components/client-zeile.ts |  135 +++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 135 insertions(+), 0 deletions(-)

diff --git a/desktop/frontend/src/components/client-zeile.ts b/desktop/frontend/src/components/client-zeile.ts
new file mode 100644
index 0000000..29656d3
--- /dev/null
+++ b/desktop/frontend/src/components/client-zeile.ts
@@ -0,0 +1,135 @@
+import { LitElement, html, nothing } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+import { event, type EventRef } from '@dh-software/lit-extensions';
+import type { EintragMitBefund, LokalerZustand } from '../bruecke';
+
+/** Was der jeweilige Zustand für den Nutzer bedeutet. */
+const ZUSTAND_TEXT: Record<LokalerZustand, string> = {
+  fehlt: 'nicht auf diesem Rechner',
+  aktuell: 'aktuell',
+  veraltet: 'veraltet',
+  fremd: 'von Hand verändert',
+};
+
+const ZUSTAND_KLASSE: Record<LokalerZustand, string> = {
+  fehlt: 'zustand-fehlt',
+  aktuell: 'zustand-aktuell',
+  veraltet: 'zustand-veraltet',
+  fremd: 'zustand-fremd',
+};
+
+/**
+ * Eine Zeile des Katalogs im Client. Anders als in der Weboberfläche steht hier,
+ * was auf diesem Rechner liegt — das ist der ganze Unterschied.
+ */
+@customElement('client-zeile')
+export class ClientZeile extends LitElement {
+  @property({ attribute: false }) eintrag!: EintragMitBefund;
+  @property({ type: Boolean }) laeuft = false;
+
+  @event({ bubbles: true, composed: true }, 'holen')
+  private holen!: EventRef<EintragMitBefund>;
+
+  @event({ bubbles: true, composed: true }, 'verknuepfen')
+  private verknuepfen!: EventRef<EintragMitBefund>;
+
+  // Light DOM, damit das globale dh-components-Theme greift.
+  createRenderRoot() {
+    return this;
+  }
+
+  render() {
+    const eintrag = this.eintrag;
+    return html`
+      <div class="zeile">
+        <div class="zeile-info">
+          <span class="zeile-name">
+            ${eintrag.name}
+            ${eintrag.einstufung === 'lizenzpflichtig'
+              ? html`<span class="marke lizenz-marke">lizenzpflichtig</span>`
+              : nothing}
+            ${eintrag.einstufung === 'intern' ? html`<span class="marke intern-marke">intern</span>` : nothing}
+          </span>
+          <span class="zeile-meta">${this.rendereStand()}${this.rendereZustand()}</span>
+          ${eintrag.hinweis ? html`<span class="zeile-hinweis">${eintrag.hinweis}</span>` : nothing}
+        </div>
+        <div class="zeile-aktionen">${this.rendereAktion()}${this.rendereVerknuepfung()}</div>
+      </div>
+    `;
+  }
+
+  private rendereStand() {
+    const zeitpunkt = this.eintrag.veroeffentlichtAm || this.eintrag.dateiStandVom;
+    if (!zeitpunkt) {
+      return nothing;
+    }
+    const datum = new Date(zeitpunkt);
+    if (Number.isNaN(datum.getTime())) {
+      return nothing;
+    }
+    return html`Stand ${datum.toLocaleDateString('de-DE')}`;
+  }
+
+  /** Nur bei eigenständigen Programmen aussagekräftig — Installer verraten hier nichts. */
+  private rendereZustand() {
+    const befund = this.eintrag.lokal;
+    if (!befund) {
+      return nothing;
+    }
+    return html` &nbsp;·&nbsp;
+      <span class=${ZUSTAND_KLASSE[befund.zustand]}>${ZUSTAND_TEXT[befund.zustand]}</span>`;
+  }
+
+  /**
+   * Ein rundes Symbol wie in der Weboberfläche. Was der Klick bewirkt, steht
+   * ohnehin schon als Zustand in der Zeile — die Beschriftung wäre eine
+   * Wiederholung. Steht ein Update an, ist der Knopf gefüllt statt flach.
+   */
+  private rendereAktion() {
+    const eintrag = this.eintrag;
+    if (!eintrag.dateiname) {
+      return html`<span class="zeile-hinweis">keine Datei hinterlegt</span>`;
+    }
+    const zustand = eintrag.lokal?.zustand;
+    const stehtAn = zustand === 'veraltet' || zustand === 'fremd';
+    const zweck = stehtAn ? 'aktualisieren' : zustand === 'aktuell' ? 'erneut holen' : 'holen';
+    return html`<dh-button
+      icon-button
+      variant=${stehtAn ? 'filled' : 'standard'}
+      title=${`${eintrag.name} ${zweck}`}
+      ?disabled=${this.laeuft}
+      @click=${this.beiHolen}
+    >
+      <span slot="img" class="fv-icon-download"></span>
+    </dh-button>`;
+  }
+
+  private rendereVerknuepfung() {
+    const befund = this.eintrag.lokal;
+    if (!befund || befund.zustand === 'fehlt') {
+      return nothing;
+    }
+    return html`<dh-button
+      icon-button
+      variant="standard"
+      title="Verknüpfung auf dem Desktop anlegen"
+      @click=${this.beiVerknuepfen}
+    >
+      <span slot="img" class="fv-icon-link"></span>
+    </dh-button>`;
+  }
+
+  private beiHolen(): void {
+    this.holen.dispatch(this.eintrag);
+  }
+
+  private beiVerknuepfen(): void {
+    this.verknuepfen.dispatch(this.eintrag);
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'client-zeile': ClientZeile;
+  }
+}

--
Gitblit v1.9.3