Server-Bibliothek
vor 17 Std. 98bce1b0da675715b2e5482c0ad61ba21135f62f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import { LitElement, html, nothing } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { event, type EventRef } from '@dh-software/lit-extensions';
import type { Software } from 'bibliothek-types';
 
/**
 * Eine kompakte Katalog-Zeile für einen Software-Eintrag. Feuert Aktions-Events an die App.
 * Die Aktionen liegen in festen Spalten, damit sie über alle Zeilen hinweg untereinander stehen;
 * fehlt eine Aktion, bleibt ihre Spalte leer.
 *
 * Zusatzinhalte (Plug-ins, Sprachpakete, Vorlagen …) stehen eingerückt direkt unter ihrem Tool
 * und bilden mit ihm eine Gruppe.
 */
@customElement('bibliothek-zeile')
export class BibliothekZeile extends LitElement {
  @property({ attribute: false }) software!: Software;
  /** Weitere Einträge, die zu dieser Software gehören. */
  @property({ attribute: false }) zusatzinhalte: Software[] = [];
  @property({ type: Boolean }) lokal = false;
  /** Zeile eines Zusatzinhalts (eingerückt unter seinem Tool). */
  @property({ type: Boolean }) eingerueckt = false;
  /** Ist dieser Eintrag für den Sammel-Download angehakt? */
  @property({ type: Boolean }) gewaehlt = false;
  /** Ids aller angehakten Einträge — nötig, damit auch eingerückte Zeilen ihren Zustand kennen. */
  @property({ attribute: false }) auswahl: ReadonlySet<number> = new Set();
 
  @event({ bubbles: true, composed: true }, 'auswahlGeaendert')
  private auswahlGeaendert!: EventRef<{ software: Software; gewaehlt: boolean }>;
 
  @event({ bubbles: true, composed: true }, 'herunterladen')
  private herunterladen!: EventRef<Software>;
 
  @event({ bubbles: true, composed: true }, 'pruefen')
  private pruefen!: EventRef<Software>;
 
  @event({ bubbles: true, composed: true }, 'bearbeiten')
  private bearbeiten!: EventRef<Software>;
 
  @event({ bubbles: true, composed: true }, 'loeschen')
  private loeschen!: EventRef<Software>;
 
  // Light DOM, damit das globale dh-components-Theme greift.
  createRenderRoot() {
    return this;
  }
 
  render() {
    const software = this.software;
    return html`
      <div class="zeile ${this.eingerueckt ? 'zeile-eingerueckt' : ''}">
        <span class="zeile-auswahl">
          <dh-checkbox
            ?checked=${this.gewaehlt}
            ?disabled=${!software.dateiname}
            title=${software.dateiname ? 'Für Sammel-Download auswählen' : 'Keine Datei vorhanden'}
            @isChange=${this.beiAuswahl}
          ></dh-checkbox>
        </span>
        <div class="zeile-info">
          <span class="zeile-name">
            ${software.name}
            ${software.einstufung === 'lizenzpflichtig'
              ? html`<span class="lizenz-marke" title="Lizenzschlüssel über den Support beziehen">lizenzpflichtig</span>`
              : nothing}
            ${software.einstufung === 'intern' ? html`<span class="intern-marke">intern</span>` : nothing}
          </span>
          <span class="zeile-meta">
            ${this.rendereStand()}
            <span class="${this.statusKlasse()}">${this.statusText()}</span>
            ${this.rendereSignatur()}
          </span>
          ${this.rendereHinweise()}
        </div>
        <div class="zeile-aktionen">
          <span class="aktion">
            ${software.anleitungUrl
              ? html`<a class="anleitung" href=${software.anleitungUrl} target="_blank" rel="noopener noreferrer">Anleitung</a>`
              : nothing}
          </span>
          <span class="aktion">
            ${software.dateiname
              ? html`<dh-button
                  icon-button
                  variant="filled"
                  title=${`${software.name} herunterladen`}
                  @click=${this.beiHerunterladen}
                >
                  <span slot="img" class="fv-icon-download"></span>
                </dh-button>`
              : nothing}
          </span>
          ${this.lokal
            ? html`
                <span class="aktion">
                  ${software.quelleTyp && software.quelleTyp !== 'manuell'
                    ? html`<dh-button buttonText="Prüfen" variant="standard" @click=${this.beiPruefen}></dh-button>`
                    : nothing}
                </span>
                <span class="aktion">
                  <dh-button buttonText="Bearbeiten" variant="standard" @click=${this.beiBearbeiten}></dh-button>
                </span>
                <span class="aktion">
                  <dh-button buttonText="Löschen" variant="standard" @click=${this.beiLoeschen}></dh-button>
                </span>
              `
            : nothing}
        </div>
      </div>
      ${this.zusatzinhalte.map(
        (zusatzinhalt) => html`<bibliothek-zeile
          .software=${zusatzinhalt}
          .auswahl=${this.auswahl}
          ?gewaehlt=${this.auswahl.has(zusatzinhalt.id)}
          ?lokal=${this.lokal}
          eingerueckt
        ></bibliothek-zeile>`
      )}
    `;
  }
 
  private beiAuswahl(ereignis: CustomEvent<boolean>): void {
    this.auswahlGeaendert.dispatch({ software: this.software, gewaehlt: ereignis.detail });
  }
 
  /**
   * Hinweise zum Umgang mit der Datei: was aus der Programmart folgt, plus der
   * freie Hinweis am Eintrag. Die eigene Zeile kostet nur dort Höhe, wo wirklich
   * etwas zu sagen ist — die meisten Einträge bleiben einzeilig.
   */
  private rendereHinweise() {
    const software = this.software;
    const teile: string[] = [];
    if (software.dateiname && software.programmart === 'installer') {
      teile.push('Datei kann nach der Installation gelöscht werden');
    }
    if (software.dateiname && software.programmart === 'eigenstaendig') {
      teile.push('Läuft direkt — an einen festen Ort legen');
    }
    if (software.hinweis) {
      teile.push(software.hinweis);
    }
    return teile.length > 0 ? html`<span class="zeile-hinweis">${teile.join(' · ')}</span>` : nothing;
  }
 
  /**
   * Signaturbefund der bereitgestellten Datei — nur in der Verwaltung.
   * Für die Netzwerk-Ansicht ist das Betriebswissen ohne Nutzen: Wer etwas
   * herunterlädt, kann daraus keine Entscheidung ableiten.
   */
  private rendereSignatur() {
    const software = this.software;
    if (!this.lokal || !software.dateiname || !software.signaturStatus) {
      return nothing;
    }
    if (software.signaturStatus === 'Valid') {
      return html` &nbsp;·&nbsp;
        <span class="signatur-gut" title="Gültig signiert von ${software.signaturHerausgeber}">
          signiert: ${software.signaturHerausgeber}
        </span>`;
    }
    const text = software.signaturStatus === 'NotSigned' ? 'unsigniert' : `Signatur unklar (${software.signaturStatus})`;
    return html` &nbsp;·&nbsp; <span class="signatur-unklar" title="Windows-Signaturstatus: ${software.signaturStatus}">${text}</span>`;
  }
 
  /**
   * Zeigt bevorzugt das Veröffentlichungsdatum des Anbieters — das ist die Angabe,
   * die den Nutzer interessiert. Nur wenn die Quelle keins liefert, wird ersatzweise
   * unser Abrufzeitpunkt genannt (und als solcher benannt).
   */
  private rendereStand() {
    const software = this.software;
    if (software.veroeffentlichtAm) {
      return html`Stand ${this.alsDatum(software.veroeffentlichtAm)} &nbsp;·&nbsp; `;
    }
    if (software.dateiStandVom) {
      return html`geholt ${this.alsDatum(software.dateiStandVom)} &nbsp;·&nbsp; `;
    }
    return nothing;
  }
 
  private alsDatum(zeitstempel: string): string {
    const zeitpunkt = new Date(zeitstempel);
    return Number.isNaN(zeitpunkt.getTime()) ? '—' : zeitpunkt.toLocaleDateString('de-DE');
  }
 
  private statusText(): string {
    const software = this.software;
    if (software.dateiname) {
      return software.dateiname;
    }
    if (!software.quelleTyp) {
      return 'keine Quelle';
    }
    if (software.quelleTyp === 'manuell') {
      return 'keine Datei hinterlegt';
    }
    if (software.letzterAbgleich && !software.abgleichErfolg) {
      return software.abgleichMeldung || 'Abgleich fehlgeschlagen';
    }
    return 'Quelle hinterlegt, noch nicht geladen';
  }
 
  private statusKlasse(): string {
    const software = this.software;
    if (software.dateiname) {
      return 'status-ok';
    }
    if (!software.quelleTyp || software.quelleTyp === 'manuell' || (software.letzterAbgleich && !software.abgleichErfolg)) {
      return 'status-fehler';
    }
    return 'status-ausstehend';
  }
 
  private beiHerunterladen(): void {
    this.herunterladen.dispatch(this.software);
  }
 
  private beiPruefen(): void {
    this.pruefen.dispatch(this.software);
  }
 
  private beiBearbeiten(): void {
    this.bearbeiten.dispatch(this.software);
  }
 
  private beiLoeschen(): void {
    this.loeschen.dispatch(this.software);
  }
}
 
declare global {
  interface HTMLElementTagNameMap {
    'bibliothek-zeile': BibliothekZeile;
  }
}