Initial commit: Angular PV Pulse App
This commit is contained in:
229
src/app/features/settings/settings.component.html
Normal file
229
src/app/features/settings/settings.component.html
Normal file
@@ -0,0 +1,229 @@
|
||||
<div class="settings-page">
|
||||
|
||||
<header class="settings-header">
|
||||
<button type="button" class="btn-back" (click)="goBack()" aria-label="Zurück zum Dashboard">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
|
||||
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/>
|
||||
</svg>
|
||||
Zurück
|
||||
</button>
|
||||
<h1>
|
||||
<svg class="settings-header__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 15.5a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm7.43-2.03c.04-.32.07-.65.07-.97s-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11.5c-.04.34-.07.67-.07 1s.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.58 1.69-.98l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66z"/>
|
||||
</svg>
|
||||
Einstellungen
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<form [formGroup]="form" (ngSubmit)="save()" class="settings-form" novalidate>
|
||||
|
||||
<!-- ── API-Konfiguration ─────────────────────────────── -->
|
||||
<section class="settings-section">
|
||||
<h2 class="section-title">API-Konfiguration</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="apiBaseUrl">API Basis-URL</label>
|
||||
<input
|
||||
id="apiBaseUrl"
|
||||
type="url"
|
||||
formControlName="apiBaseUrl"
|
||||
placeholder="https://api.example.com/api"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (form.controls.apiBaseUrl.invalid && form.controls.apiBaseUrl.touched) {
|
||||
<span class="field-error">Pflichtfeld – bitte eine gültige URL eingeben</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group--checkbox">
|
||||
<input id="useMocks" type="checkbox" formControlName="useMocks" />
|
||||
<label for="useMocks">Mock-Modus aktiv (kein echter API-Aufruf)</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pollingIntervalMs">Abfrageintervall Live-Daten (ms)</label>
|
||||
<input
|
||||
id="pollingIntervalMs"
|
||||
type="number"
|
||||
formControlName="pollingIntervalMs"
|
||||
min="1000"
|
||||
step="500"
|
||||
/>
|
||||
@if (form.controls.pollingIntervalMs.invalid && form.controls.pollingIntervalMs.touched) {
|
||||
<span class="field-error">Minimum 1000 ms</span>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Endpunkte ────────────────────────────────────── -->
|
||||
<section class="settings-section">
|
||||
<h2 class="section-title">Endpunkte</h2>
|
||||
<p class="section-hint">Verwende <code>{id}</code> als Platzhalter für die Anlagen-ID</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="endpointPlants">Anlagenliste</label>
|
||||
<input
|
||||
id="endpointPlants"
|
||||
type="text"
|
||||
formControlName="endpointPlants"
|
||||
placeholder="/plants"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (form.controls.endpointPlants.invalid && form.controls.endpointPlants.touched) {
|
||||
<span class="field-error">Pflichtfeld</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="endpointLiveData">Live-Daten</label>
|
||||
<input
|
||||
id="endpointLiveData"
|
||||
type="text"
|
||||
formControlName="endpointLiveData"
|
||||
placeholder="/plants/{id}/live"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (form.controls.endpointLiveData.invalid && form.controls.endpointLiveData.touched) {
|
||||
<span class="field-error">Pflichtfeld</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="endpointHistory">Historien-Daten</label>
|
||||
<input
|
||||
id="endpointHistory"
|
||||
type="text"
|
||||
formControlName="endpointHistory"
|
||||
placeholder="/plants/{id}/history"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (form.controls.endpointHistory.invalid && form.controls.endpointHistory.touched) {
|
||||
<span class="field-error">Pflichtfeld</span>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Authentifizierung ─────────────────────────────── -->
|
||||
<section class="settings-section">
|
||||
<h2 class="section-title">Authentifizierung</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="authEndpoint">Auth-Endpunkt (relativer Pfad)</label>
|
||||
<input
|
||||
id="authEndpoint"
|
||||
type="text"
|
||||
formControlName="authEndpoint"
|
||||
placeholder="/auth/token"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (form.controls.authEndpoint.invalid && form.controls.authEndpoint.touched) {
|
||||
<span class="field-error">Pflichtfeld</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">Benutzername</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
formControlName="username"
|
||||
autocomplete="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Passwort</label>
|
||||
<div class="input-with-action">
|
||||
<input
|
||||
id="password"
|
||||
[type]="showPassword() ? 'text' : 'password'"
|
||||
formControlName="password"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon"
|
||||
(click)="showPassword.set(!showPassword())"
|
||||
[attr.aria-label]="showPassword() ? 'Passwort verbergen' : 'Passwort anzeigen'"
|
||||
title="{{ showPassword() ? 'Passwort verbergen' : 'Passwort anzeigen' }}"
|
||||
>
|
||||
@if (showPassword()) {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
|
||||
<path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46A11.804 11.804 0 0 0 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78 3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/>
|
||||
</svg>
|
||||
} @else {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
|
||||
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8a3 3 0 1 0 0 6 3 3 0 0 0 0-6z"/>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token-Status -->
|
||||
<div class="token-card">
|
||||
<div class="token-card__status">
|
||||
@if (token()) {
|
||||
<div class="token-indicator token-indicator--active">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
</svg>
|
||||
<span>Token aktiv</span>
|
||||
</div>
|
||||
<code class="token-value">{{ maskedToken }}</code>
|
||||
<button type="button" class="btn-ghost btn-sm" (click)="clearToken()">Löschen</button>
|
||||
} @else {
|
||||
<div class="token-indicator token-indicator--missing">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
||||
</svg>
|
||||
<span>Kein Token gesetzt</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (tokenStatus() === 'loading') {
|
||||
<p class="token-card__msg token-card__msg--info">Token wird abgerufen…</p>
|
||||
}
|
||||
@if (tokenStatus() === 'success') {
|
||||
<p class="token-card__msg token-card__msg--success">✓ Token erfolgreich abgerufen</p>
|
||||
}
|
||||
@if (tokenStatus() === 'error') {
|
||||
<p class="token-card__msg token-card__msg--error">✗ {{ tokenError() }}</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary"
|
||||
(click)="fetchToken()"
|
||||
[disabled]="!form.controls.username.value || !form.controls.password.value || tokenStatus() === 'loading'"
|
||||
>
|
||||
@if (tokenStatus() === 'loading') {
|
||||
<span class="btn-spinner"></span>
|
||||
Token wird abgerufen…
|
||||
} @else {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
|
||||
<path d="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4l4-4-4-4v4h-4.35zM7 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z"/>
|
||||
</svg>
|
||||
Token abrufen
|
||||
}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<!-- ── Aktionen ──────────────────────────────────────── -->
|
||||
<div class="settings-actions">
|
||||
@if (saveSuccess()) {
|
||||
<span class="save-success">✓ Einstellungen gespeichert</span>
|
||||
}
|
||||
<button type="button" class="btn-secondary" (click)="goBack()">Abbrechen</button>
|
||||
<button type="submit" class="btn-primary" [disabled]="form.invalid">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
|
||||
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/>
|
||||
</svg>
|
||||
Einstellungen speichern
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
Reference in New Issue
Block a user