fix(ui): Zugang zur Laufzeit auf dem Telefon (#039)

Im Reiter Rechnungen der Host-Detailansicht war die Laufzeit nie verlinkt,
und in der Rechnungsliste ist diese Spalte erst ab 992 px sichtbar. Auf dem
Telefon fuehrte damit kein Weg zu den Rechnungsformularen.

Die Laufzeit ist jetzt ein Link und zugleich Ueberschrift der Karte; in der
Liste bleibt sie auf dem Telefon sichtbar und entfaellt nur zwischen 768 und
992 px. Dafuer braucht es eine eigene Klasse: Bootstraps d-*-Hilfsklassen
setzen display mit !important und ueberschreiben das Kartenlayout.

Nebenbefund: Auch dieser Reiter rechnete den Einkauf ohne Rabatt - dritte
Stelle nach #037 und #038.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-03 17:08:54 +02:00
co-authored by Claude Opus 5
parent e570d30019
commit 335fc47496
16 changed files with 81 additions and 15 deletions
+9
View File
@@ -17,6 +17,15 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
### Sicherheit
## [0.22.2] – 2026-10-03
### Behoben
- **Auf dem Telefon führte kein Weg zu den Rechnungsformularen**: Im Reiter *Rechnungen* der Host-Detailansicht war die Laufzeit nicht verlinkt, und in der Rechnungsliste ist diese Spalte erst ab 992 px sichtbar (#039)
- Der Reiter *Rechnungen* rechnete den Einkauf noch ohne Rabatt (#039)
### Geändert
- Der Reiter *Rechnungen* nutzt auf dem Telefon das Kartenlayout (#039)
## [0.22.1] – 2026-10-03
### Behoben
+7 -2
View File
@@ -2,7 +2,7 @@
## Zeitübersicht
**Gesamt:** 20 h 33 min
**Gesamt:** 21 h 03 min
| Issue | Zeit |
|---|---|
@@ -44,6 +44,7 @@
| #036 Standort am Host | 0 h 30 min |
| #037 Rabatt je Position | 0 h 55 min |
| #038 Überbreite auf dem Handy | 0 h 40 min |
| #039 Zugang zur Laufzeit | 0 h 30 min |
| Meilenstein | Zeit |
|---|---|
@@ -66,7 +67,7 @@
## 2026-09-17 · Sitzung 1 · 19:36–laufend
**Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0)
**Version:** 0.22.1
**Version:** 0.22.2
### Erledigt
- Auftrag gelesen, Rückfragen gestellt
@@ -199,6 +200,10 @@
- Dabei einen eigenen Fehler aus #037 gefunden: Die Detailseite rechnete weiter mit dem Listenpreis. Beim Ändern einer Rechengröße reicht es nicht, die Abfragen anzupassen – auch die Seiten rechnen selbst
- Das Aufnahme-Skript kann jetzt gezielt in Telefonbreite aufnehmen; damit ließ sich die Behebung belegen statt nur behaupten
- #039: Auf dem Telefon führte kein Weg zu den Rechnungsformularen – die Laufzeit war nicht verlinkt und die Spalte erst ab 992 px sichtbar
- Dritte Stelle, die den Einkauf ohne Rabatt rechnete. Beim Ändern einer Rechengröße gehört jede Stelle gesucht, die sie selbst bildet – nicht nur die Abfragen
- Bootstraps `d-*`-Klassen setzen `display` mit `!important`; für Spalten in der Kartenansicht braucht es eigene Klassen
### Nächster Schritt
- #016 Benutzerhandbuch mit Screenshots, danach #017 Favicons und #019 CI-Pipeline
- Für 1.0.0 fehlt noch: PMG (kein Host verfügbar) und der Fall „aktive Subscription auf PBS“ (#023)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 177 KiB

+32
View File
@@ -0,0 +1,32 @@
# #039 Auf dem Telefon führte kein Weg zur Laufzeit
**Status:** erledigt
**Meilenstein:** v0.22.2
**Labels:** bug, ui
**Geschätzt:** 1 h **Tatsächlich:** 0 h 30 min
## Auslöser
„Kann ich am Handy nicht rein.“ Gemeint war der Reiter *Rechnungen* in der Host-Detailansicht: Die
Zeile zeigt Laufzeit, Rechnung und Beträge – aber nichts davon war anklickbar.
## Ursache
Zwei Stellen:
1. In der Host-Detailansicht war die Laufzeit nie verlinkt, auch am Rechner nicht. Man kam nur über
die Rechnungsliste dorthin.
2. In der Rechnungsliste ist die Laufzeit-Spalte erst ab 992 px eingeblendet. Auf dem Telefon fehlte
damit der einzige Verweis auf die Formulare – die Karte verwies nur auf den Host.
## Behebung
- Die Laufzeit in der Host-Detailansicht ist ein Link und zugleich die Überschrift der Karte
- In der Rechnungsliste bleibt die Laufzeit auf dem Telefon sichtbar; ausgeblendet wird sie nur
zwischen 768 und 992 px, wo die Tabelle am engsten ist
- Der Reiter *Rechnungen* nutzt ebenfalls das Kartenlayout
## Nebenbefund
Auch dieser Reiter rechnete den Einkauf noch ohne Rabatt – 1.100 € statt 990 €. Dritte Stelle nach
#037 und #038; alle gefunden, weil Florian die Zahl auf dem Bildschirm mit seiner Rechnung verglichen hat.
## Gelernt
Bootstraps `d-*`-Hilfsklassen setzen `display` mit `!important` und überschreiben damit das
Kartenlayout. Für Spalten, die in der Kartenansicht sichtbar bleiben sollen, braucht es eine eigene
Klasse (`pl-spalte-laufzeit`).
+2 -1
View File
@@ -42,4 +42,5 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a
| [#036](036-standort-am-host.md) | Standort am Host setzen | erledigt | v0.21.0 | 1 h | 0 h 30 min |
| [#037](037-rabatt-und-ablauf.md) | Rabatt je Position und der tatsächliche Ablauf | erledigt | v0.22.0 | 3 h | 0 h 55 min |
| [#038](038-ueberbreite-handy.md) | Seite wird auf dem Handy herausgezoomt | erledigt | v0.22.1 | 2 h | 0 h 40 min |
| | **Summe** | | | **162 h** | **20 h 33 min** |
| [#039](039-zugang-zur-laufzeit.md) | Auf dem Telefon führte kein Weg zur Laufzeit | erledigt | v0.22.2 | 1 h | 0 h 30 min |
| | **Summe** | | | **163 h** | **21 h 03 min** |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "proxledger",
"version": "0.22.1",
"version": "0.22.2",
"private": true,
"scripts": {
"dev": "next dev",
+15 -8
View File
@@ -346,7 +346,7 @@ export default async function HostDetailSeite({ params }: { params: Promise<{ id
icon: "bi-receipt",
inhalt: (
<div className="table-responsive">
<table className="table table-striped align-middle mb-0">
<table className="table table-striped align-middle mb-0 pl-karten-tabelle">
<thead>
<tr>
<th scope="col">Laufzeit</th>
@@ -359,21 +359,28 @@ export default async function HostDetailSeite({ params }: { params: Promise<{ id
</thead>
<tbody>
{host.laufzeiten.map((laufzeit) => {
// Nach Abzug des Rabatts – das ist, was tatsächlich gezahlt wird (#037).
const einkauf = laufzeit.proxmoxPosition
? Number(laufzeit.proxmoxPosition.nettoBetrag)
? Number(laufzeit.proxmoxPosition.nettoBetrag) -
Number(laufzeit.proxmoxPosition.rabatt ?? 0)
: null;
const verkauf = laufzeit.kundenPosition
? Number(laufzeit.kundenPosition.nettoBetrag)
: null;
return (
<tr key={laufzeit.id}>
<td>
<td className="pl-karten-kopf">
<Link
href={`/rechnungen/${laufzeit.id}`}
className="fw-semibold text-decoration-none"
>
{formatDatum(laufzeit.start)} – {formatDatum(laufzeit.ende)}
</Link>
{laufzeit.status === "VORSCHLAG" ? (
<span className="badge pl-bg-info ms-2">Vorschlag</span>
) : null}
</td>
<td>
<td data-label="Proxmox-Rechnung">
<CopyableCode wert={laufzeit.proxmoxPosition?.rechnung.nummer ?? null} />
<div>
<EinkaufBadge
@@ -385,8 +392,8 @@ export default async function HostDetailSeite({ params }: { params: Promise<{ id
/>
</div>
</td>
<td>{formatEuro(einkauf)}</td>
<td>
<td data-label="Einkauf">{formatEuro(einkauf)}</td>
<td data-label="Kundenrechnung">
<CopyableCode wert={laufzeit.kundenPosition?.rechnung.nummer ?? null} />
<div>
<VerkaufBadge
@@ -395,8 +402,8 @@ export default async function HostDetailSeite({ params }: { params: Promise<{ id
/>
</div>
</td>
<td>{formatEuro(verkauf)}</td>
<td>
<td data-label="Verkauf">{formatEuro(verkauf)}</td>
<td data-label="Marge">
{einkauf !== null && verkauf !== null
? formatEuro(verkauf - einkauf)
: "–"}
+2 -2
View File
@@ -204,7 +204,7 @@ export default async function RechnungenSeite({ searchParams }: { searchParams:
<th scope="col" className="d-none d-md-table-cell">
Host
</th>
<th scope="col" className="d-none d-lg-table-cell">
<th scope="col" className="pl-spalte-laufzeit">
Laufzeit
</th>
<th scope="col">Proxmox-Rechnung</th>
@@ -254,7 +254,7 @@ export default async function RechnungenSeite({ searchParams }: { searchParams:
{zeile.hostname}
</Link>
</td>
<td className="d-none d-lg-table-cell" data-label="Laufzeit">
<td className="pl-spalte-laufzeit" data-label="Laufzeit">
<Link href={`/rechnungen/${zeile.id}`} className="text-decoration-none">
{formatDatum(zeile.start)} – {formatDatum(zeile.ende)}
</Link>
+12
View File
@@ -401,6 +401,18 @@ body {
max-width: calc(100vw - 1rem);
}
/*
* Die Laufzeit-Spalte ist auf dem Telefon der einzige Weg zu den Rechnungsformularen, deshalb
* bleibt sie dort sichtbar. Zwischen 768 und 992 px ist die Tabelle am engsten – dort entfällt sie.
* Bewusst eine eigene Klasse statt der Bootstrap-Hilfsklassen: deren `display: … !important`
* überschreibt das Kartenlayout (#039).
*/
@media (min-width: 768px) and (max-width: 991.98px) {
.pl-spalte-laufzeit {
display: none;
}
}
/* ---------- Mobil ---------- */
@media (max-width: 575.98px) {