Zeitraum im Kartenkopf; Verlauf feiner als ein Punkt pro Tag

Zeitraum-Auswahl:
Sie stand ueber dem Diagramm und nahm ihm Hoehe weg. Jetzt sitzt sie klein im
Kartenkopf neben dem Titel - die ganze Kartenhoehe gehoert dem Diagramm. Die
Wahl wird in der Anordnung mitgespeichert, bleibt also nach dem Neuladen
erhalten; ausserhalb des Bearbeitungsmodus geschieht das still im Hintergrund.

Feinere Aufloesung:
Ein Punkt je Tag verbarg, WANN ein- und ausgelagert wurde. Die Abtastrate
richtet sich jetzt nach dem Zeitraum:
  bis  2 Tage  -> stuendlich
  bis 14 Tage  -> alle 6 Stunden
  bis 120 Tage -> taeglich
  darueber     -> woechentlich
So bleibt die Punktzahl immer zwischen etwa 25 und 105 - fein genug zum
Erkennen, grob genug zum Zeichnen. Ein Test haelt diese Spanne fest.

Die Zeitachse traegt entsprechend Uhrzeit statt Datum, wenn stuendlich
abgetastet wird. Punkte heissen jetzt "at" (Zeitpunkt) statt "date".
Zeitstempel aus SQLite kommen ohne Zeitzone zurueck und werden vereinheitlicht,
sonst schluege die Differenzbildung fehl.

Auswahl erweitert: 24 Stunden, 2 Tage, 7, 30, 90 Tage, 6 Monate, 1 Jahr.

Geprueft: "npm run build" laeuft durch. pytest weiterhin nicht ausfuehrbar -
kein Python auf diesem Rechner; die Tests wurden an die neue Aufloesung
angepasst.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-23 10:36:33 +02:00
parent 97a6e94ea1
commit 1fd1e89872
6 changed files with 199 additions and 115 deletions

View File

@@ -301,6 +301,35 @@ def _signiert(movement: Movement) -> float:
return movement.quantity # Korrekturen sind bereits vorzeichenbehaftet return movement.quantity # Korrekturen sind bereits vorzeichenbehaftet
def _utc(zeitpunkt: datetime) -> datetime:
"""SQLite gibt Zeitstempel ohne Zeitzone zurück hier vereinheitlichen."""
return zeitpunkt if zeitpunkt.tzinfo else zeitpunkt.replace(tzinfo=timezone.utc)
def _schrittweite(days: int) -> timedelta:
"""Abtastrate passend zum Zeitraum.
Ein Punkt je Tag verbirgt, wann am Tag etwas passiert ist; ein Punkt je
Stunde über ein Jahr wären knapp 9000 Punkte. Deshalb gestaffelt die
Anzahl der Punkte bleibt so immer im Bereich von etwa 25 bis 170.
"""
if days <= 2:
return timedelta(hours=1)
if days <= 14:
return timedelta(hours=6)
if days <= 120:
return timedelta(days=1)
return timedelta(days=7)
def _eimer(days: int) -> tuple[datetime, timedelta, int]:
"""Endzeitpunkt, Schrittweite und Anzahl der Abschnitte."""
schritt = _schrittweite(days)
jetzt = datetime.now(timezone.utc)
anzahl = max(1, int(timedelta(days=days) / schritt))
return jetzt, schritt, anzahl
@router.get("/timeline", response_model=list[TimelinePoint]) @router.get("/timeline", response_model=list[TimelinePoint])
def timeline( def timeline(
days: int = 90, days: int = 90,
@@ -317,7 +346,7 @@ def timeline(
Packungsgröße. Wird sie später geändert, verschiebt sich auch die Historie. Packungsgröße. Wird sie später geändert, verschiebt sich auch die Historie.
""" """
days = max(1, min(days, 730)) days = max(1, min(days, 730))
heute = date.today() jetzt, schritt, anzahl = _eimer(days)
produkte = db.query(Product) produkte = db.query(Product)
if product_id is not None: if product_id is not None:
@@ -329,26 +358,29 @@ def timeline(
faktoren = {p.id: (article_unit(p)[0] or 1.0) for p in produkte} faktoren = {p.id: (article_unit(p)[0] or 1.0) for p in produkte}
bestand = {p.id: current_stock(db, p.id) for p in produkte} bestand = {p.id: current_stock(db, p.id) for p in produkte}
beginn = datetime.combine(heute - timedelta(days=days - 1), datetime.min.time(), tzinfo=timezone.utc) beginn = jetzt - schritt * anzahl
bewegungen = db.query(Movement).filter(Movement.created_at >= beginn) bewegungen = db.query(Movement).filter(Movement.created_at >= beginn)
if product_id is not None: if product_id is not None:
bewegungen = bewegungen.filter(Movement.product_id == product_id) bewegungen = bewegungen.filter(Movement.product_id == product_id)
delta: dict[date, dict[int, float]] = defaultdict(lambda: defaultdict(float)) # Abschnitt 0 ist der jüngste (von jetzt rückwärts eine Schrittweite).
sekunden = schritt.total_seconds()
delta: dict[int, dict[int, float]] = defaultdict(lambda: defaultdict(float))
for m in bewegungen.all(): for m in bewegungen.all():
if m.product_id in faktoren: if m.product_id not in faktoren:
delta[m.created_at.date()][m.product_id] += _signiert(m) continue
index = int((jetzt - _utc(m.created_at)).total_seconds() // sekunden)
if 0 <= index < anzahl:
delta[index][m.product_id] += _signiert(m)
def summe() -> float: def summe() -> float:
return round(sum(bestand[pid] / faktoren[pid] for pid in bestand), 3) return round(sum(bestand[pid] / faktoren[pid] for pid in bestand), 3)
punkte = [TimelinePoint(date=heute, article_units=summe())] punkte = [TimelinePoint(at=jetzt, article_units=summe())]
tag = heute for i in range(anzahl):
for _ in range(days - 1): for pid, wert in delta.get(i, {}).items():
for pid, wert in delta.get(tag, {}).items():
bestand[pid] = bestand.get(pid, 0.0) - wert bestand[pid] = bestand.get(pid, 0.0) - wert
tag = tag - timedelta(days=1) punkte.append(TimelinePoint(at=jetzt - schritt * (i + 1), article_units=summe()))
punkte.append(TimelinePoint(date=tag, article_units=summe()))
punkte.reverse() punkte.reverse()
return punkte return punkte
@@ -360,25 +392,33 @@ def activity(
db: Session = Depends(get_db), db: Session = Depends(get_db),
_: User = Depends(get_current_user), _: User = Depends(get_current_user),
) -> list[ActivityPoint]: ) -> list[ActivityPoint]:
"""Anzahl der Ein- und Auslagerungen je Tag.""" """Anzahl der Ein- und Auslagerungen je Abschnitt.
Die Abschnittslänge richtet sich nach dem Zeitraum: bei ein bis zwei Tagen
stündlich, damit erkennbar wird, zu welcher Tageszeit gelagert wird.
"""
days = max(1, min(days, 365)) days = max(1, min(days, 365))
heute = date.today() jetzt, schritt, anzahl = _eimer(days)
beginn = datetime.combine(heute - timedelta(days=days - 1), datetime.min.time(), tzinfo=timezone.utc) beginn = jetzt - schritt * anzahl
sekunden = schritt.total_seconds()
ein: dict[date, int] = defaultdict(int) ein: dict[int, int] = defaultdict(int)
aus: dict[date, int] = defaultdict(int) aus: dict[int, int] = defaultdict(int)
for m in db.query(Movement).filter(Movement.created_at >= beginn).all(): for m in db.query(Movement).filter(Movement.created_at >= beginn).all():
tag = m.created_at.date() index = int((jetzt - _utc(m.created_at)).total_seconds() // sekunden)
if not 0 <= index < anzahl:
continue
if m.type == MovementType.in_: if m.type == MovementType.in_:
ein[tag] += 1 ein[index] += 1
elif m.type == MovementType.out: elif m.type == MovementType.out:
aus[tag] += 1 aus[index] += 1
# Von alt nach neu ausgeben; Abschnitt 0 ist der jüngste.
return [ return [
ActivityPoint( ActivityPoint(
date=heute - timedelta(days=days - 1 - i), at=jetzt - schritt * i,
checked_in=ein.get(heute - timedelta(days=days - 1 - i), 0), checked_in=ein.get(i, 0),
checked_out=aus.get(heute - timedelta(days=days - 1 - i), 0), checked_out=aus.get(i, 0),
) )
for i in range(days) for i in reversed(range(anzahl))
] ]

View File

@@ -431,11 +431,13 @@ class CategoryShare(BaseModel):
class TimelinePoint(BaseModel): class TimelinePoint(BaseModel):
date: date # Zeitpunkt statt Datum: Bei kurzen Zeiträumen wird stündlich abgetastet,
# damit sichtbar wird, *wann* am Tag ein- und ausgelagert wurde.
at: datetime
article_units: float article_units: float
class ActivityPoint(BaseModel): class ActivityPoint(BaseModel):
date: date at: datetime
checked_in: int checked_in: int
checked_out: int checked_out: int

View File

@@ -1,4 +1,4 @@
from datetime import date, timedelta from datetime import date, timedelta # noqa: F401 (date fuer die Zustands-Tests)
import pytest import pytest
@@ -90,19 +90,34 @@ def test_by_category_bündelt_und_sortiert(db, user):
# ----------------------------------------------------------------- Verlauf # ----------------------------------------------------------------- Verlauf
def test_schrittweite_haengt_am_zeitraum():
"""Kurze Zeitraeume werden fein abgetastet, lange grob."""
assert dashboard._schrittweite(1) == timedelta(hours=1)
assert dashboard._schrittweite(2) == timedelta(hours=1)
assert dashboard._schrittweite(7) == timedelta(hours=6)
assert dashboard._schrittweite(90) == timedelta(days=1)
assert dashboard._schrittweite(365) == timedelta(days=7)
def test_schrittweite_haelt_die_punktzahl_im_rahmen():
"""Egal welcher Zeitraum die Kurve bleibt zeichenbar."""
for tage in (1, 2, 7, 14, 30, 90, 180, 365, 730):
anzahl = timedelta(days=tage) / dashboard._schrittweite(tage)
assert 20 <= anzahl <= 180, f"{tage} Tage ergeben {anzahl} Punkte"
def test_timeline_rechnet_bewegungen_zurueck(db, rice, user): def test_timeline_rechnet_bewegungen_zurueck(db, rice, user):
"""Der Verlauf entsteht rueckwaerts aus dem heutigen Bestand.""" """Der Verlauf entsteht rueckwaerts aus dem heutigen Bestand."""
check_in(db, rice, 4, "package", None, None, user) # +4 Einheiten heute check_in(db, rice, 4, "package", None, None, user) # +4 Einheiten jetzt
check_out(db, rice, 1, "package", user) # -1 Einheit heute check_out(db, rice, 1, "package", user) # -1 Einheit jetzt
db.commit() db.commit()
punkte = dashboard.timeline(days=3, product_id=None, db=db, _=user) punkte = dashboard.timeline(days=3, product_id=None, db=db, _=user)
assert len(punkte) == 3 # Jetzt 3 Einheiten; vor dem Einlagern war das Lager leer.
# Heute: 3 Einheiten; die Tage davor lag nichts im Lager.
assert punkte[-1].date == date.today()
assert punkte[-1].article_units == 3 assert punkte[-1].article_units == 3
assert punkte[0].article_units == 0 assert punkte[0].article_units == 0
assert punkte[1].article_units == 0 # Aufsteigend sortiert, damit die Linie von links nach rechts laeuft.
assert punkte[0].at < punkte[-1].at
def test_timeline_kann_auf_ein_produkt_eingegrenzt_werden(db, rice, user): def test_timeline_kann_auf_ein_produkt_eingegrenzt_werden(db, rice, user):
@@ -125,12 +140,13 @@ def test_activity_zaehlt_ein_und_auslagerungen(db, rice, user):
check_out(db, rice, 1, "package", user) check_out(db, rice, 1, "package", user)
db.commit() db.commit()
tage = dashboard.activity(days=3, db=db, _=user) abschnitte = dashboard.activity(days=3, db=db, _=user)
assert len(tage) == 3 # Alles gerade eben passiert -> liegt im juengsten Abschnitt.
heute = tage[-1] juengster = abschnitte[-1]
assert heute.date == date.today() assert juengster.checked_in == 2
assert heute.checked_in == 2 assert juengster.checked_out == 1
assert heute.checked_out == 1 assert sum(a.checked_in for a in abschnitte) == 2
assert abschnitte[0].at < juengster.at
# ---------------------------------------------------------------- Anordnung # ---------------------------------------------------------------- Anordnung

View File

@@ -41,11 +41,18 @@ function Zustand({ fehler, daten, children, leer = "Keine Daten." }) {
return children; return children;
} }
function kurzesDatum(iso) { /**
const d = new Date(`${iso}T00:00:00`); * Beschriftung der Zeitachse. Bei kurzen Zeiträumen tastet der Server stündlich
return Number.isNaN(d.getTime()) * ab dann ist die Uhrzeit die nützliche Angabe, sonst das Datum.
? iso */
: d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit" }); function zeitLabel(iso, tage) {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return String(iso);
if (tage <= 2) return d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
if (tage <= 14) {
return d.toLocaleString("de-DE", { day: "2-digit", month: "2-digit", hour: "2-digit" });
}
return d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit" });
} }
// ---------------------------------------------------------------- Aktionen // ---------------------------------------------------------------- Aktionen
@@ -272,11 +279,14 @@ function KarteKategorienSaeulen() {
); );
} }
/** Zeitraum-Auswahl für die Verlaufskarten. */ /**
const ZEITRAEUME = [ * Auswahlmöglichkeiten für die Verlaufskarten. Die Auswahl selbst sitzt im
// 1 Tag ergibt einen einzelnen Wert das Diagramm zeigt dann die blanke Zahl * Kartenkopf (siehe Dashboard) so bleibt die ganze Kartenhöhe dem Diagramm.
// statt einer Linie aus einem Punkt. */
{ tage: 1, label: "1 Tag" }, export const ZEITRAEUME = [
// 1 Tag wird stündlich abgetastet; man sieht also, wann am Tag gelagert wurde.
{ tage: 1, label: "24 Stunden" },
{ tage: 2, label: "2 Tage" },
{ tage: 7, label: "7 Tage" }, { tage: 7, label: "7 Tage" },
{ tage: 30, label: "30 Tage" }, { tage: 30, label: "30 Tage" },
{ tage: 90, label: "90 Tage" }, { tage: 90, label: "90 Tage" },
@@ -284,28 +294,15 @@ const ZEITRAEUME = [
{ tage: 365, label: "1 Jahr" }, { tage: 365, label: "1 Jahr" },
]; ];
function ZeitraumWahl({ wert, setzen }) { function KarteBestandsverlauf({ tage = 90 }) {
return (
<select className="zeitraum" value={wert} onChange={(e) => setzen(Number(e.target.value))}>
{ZEITRAEUME.map((z) => <option key={z.tage} value={z.tage}>{z.label}</option>)}
</select>
);
}
function KarteBestandsverlauf() {
const [tage, setTage] = useState(90);
const { daten, fehler } = useDaten(() => api.dashboardTimeline(tage), [tage]); const { daten, fehler } = useDaten(() => api.dashboardTimeline(tage), [tage]);
if (fehler) return <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>;
if (!daten) return <div className="empty">Lädt</div>;
return ( return (
<div className="card-stack">
<ZeitraumWahl wert={tage} setzen={setTage} />
{fehler ? (
<div className="alert error"><Icon name="alert" size={16} />{fehler}</div>
) : !daten ? (
<div className="empty">Lädt</div>
) : (
<Line <Line
labels={daten.map((p) => kurzesDatum(p.date))} labels={daten.map((p) => zeitLabel(p.at, tage))}
serien={[{ serien={[{
name: "Artikeleinheiten", name: "Artikeleinheiten",
color: tokenFarbe("--accent", "#3f51b5"), color: tokenFarbe("--accent", "#3f51b5"),
@@ -313,15 +310,12 @@ function KarteBestandsverlauf() {
}]} }]}
einheit="Einheiten" einheit="Einheiten"
/> />
)}
</div>
); );
} }
function KarteArtikelverlauf() { function KarteArtikelverlauf({ tage = 90 }) {
const [produkte, setProdukte] = useState([]); const [produkte, setProdukte] = useState([]);
const [gewaehlt, setGewaehlt] = useState(""); const [gewaehlt, setGewaehlt] = useState("");
const [tage, setTage] = useState(90);
const { daten, fehler } = useDaten( const { daten, fehler } = useDaten(
() => (gewaehlt ? api.dashboardTimeline(tage, gewaehlt) : Promise.resolve([])), () => (gewaehlt ? api.dashboardTimeline(tage, gewaehlt) : Promise.resolve([])),
[gewaehlt, tage], [gewaehlt, tage],
@@ -337,18 +331,16 @@ function KarteArtikelverlauf() {
return ( return (
<div className="card-stack"> <div className="card-stack">
<div className="card-toolbar"> <select value={gewaehlt} onChange={(e) => setGewaehlt(e.target.value)}
<select value={gewaehlt} onChange={(e) => setGewaehlt(e.target.value)}> style={{ marginTop: 0 }}>
<option value=""> Artikel wählen </option> <option value=""> Artikel wählen </option>
{produkte.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)} {produkte.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select> </select>
<ZeitraumWahl wert={tage} setzen={setTage} />
</div>
{fehler ? ( {fehler ? (
<div className="alert error"><Icon name="alert" size={16} />{fehler}</div> <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>
) : ( ) : (
<Line <Line
labels={(daten || []).map((p) => kurzesDatum(p.date))} labels={(daten || []).map((p) => zeitLabel(p.at, tage))}
serien={[{ serien={[{
name: "Bestand", name: "Bestand",
color: tokenFarbe("--accent", "#3f51b5"), color: tokenFarbe("--accent", "#3f51b5"),
@@ -361,28 +353,21 @@ function KarteArtikelverlauf() {
); );
} }
function KarteBewegungsverlauf() { function KarteBewegungsverlauf({ tage = 30 }) {
const [tage, setTage] = useState(30);
const { daten, fehler } = useDaten(() => api.dashboardActivity(tage), [tage]); const { daten, fehler } = useDaten(() => api.dashboardActivity(tage), [tage]);
if (fehler) return <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>;
if (!daten) return <div className="empty">Lädt</div>;
return ( return (
<div className="card-stack">
<ZeitraumWahl wert={tage} setzen={setTage} />
{fehler ? (
<div className="alert error"><Icon name="alert" size={16} />{fehler}</div>
) : !daten ? (
<div className="empty">Lädt</div>
) : (
<Line <Line
labels={daten.map((p) => kurzesDatum(p.date))} labels={daten.map((p) => zeitLabel(p.at, tage))}
serien={[ serien={[
{ name: "Eingelagert", color: tokenFarbe("--ok", "#2e7d55"), werte: daten.map((p) => p.checked_in) }, { name: "Eingelagert", color: tokenFarbe("--ok", "#2e7d55"), werte: daten.map((p) => p.checked_in) },
{ name: "Ausgelagert", color: tokenFarbe("--warn", "#b06f14"), werte: daten.map((p) => p.checked_out) }, { name: "Ausgelagert", color: tokenFarbe("--warn", "#b06f14"), werte: daten.map((p) => p.checked_out) },
]} ]}
einheit="Vorgänge" einheit="Vorgänge"
/> />
)}
</div>
); );
} }
@@ -465,19 +450,19 @@ export const KARTEN = {
"stock-timeline": { "stock-timeline": {
titel: "Bestandsverlauf", icon: "history", vorschau: "linie", titel: "Bestandsverlauf", icon: "history", vorschau: "linie",
beschreibung: "Artikeleinheiten im Lager über 90 Tage", beschreibung: "Artikeleinheiten im Lager über 90 Tage",
standard: { w: 8, h: 6 }, min: { w: 4, h: 5 }, zeitraum: 90, standard: { w: 8, h: 6 }, min: { w: 4, h: 5 },
komponente: KarteBestandsverlauf, komponente: KarteBestandsverlauf,
}, },
"product-timeline": { "product-timeline": {
titel: "Verlauf eines Artikels", icon: "package", vorschau: "linie", titel: "Verlauf eines Artikels", icon: "package", vorschau: "linie",
beschreibung: "Ein wählbarer Artikel über 90 Tage", beschreibung: "Ein wählbarer Artikel über 90 Tage",
standard: { w: 8, h: 7 }, min: { w: 4, h: 6 }, zeitraum: 90, standard: { w: 8, h: 7 }, min: { w: 4, h: 6 },
komponente: KarteArtikelverlauf, komponente: KarteArtikelverlauf,
}, },
"activity-timeline": { "activity-timeline": {
titel: "Ein- und Auslagerungen", icon: "history", vorschau: "linien", titel: "Ein- und Auslagerungen", icon: "history", vorschau: "linien",
beschreibung: "Vorgänge je Tag über 30 Tage", beschreibung: "Vorgänge je Tag über 30 Tage",
standard: { w: 8, h: 6 }, min: { w: 4, h: 5 }, zeitraum: 30, standard: { w: 8, h: 6 }, min: { w: 4, h: 5 },
komponente: KarteBewegungsverlauf, komponente: KarteBewegungsverlauf,
}, },
}; };

View File

@@ -5,7 +5,7 @@ import { useAuth } from "../auth";
import Icon from "../components/Icon"; import Icon from "../components/Icon";
import { useConfirm } from "../confirm"; import { useConfirm } from "../confirm";
import { useToast } from "../toast"; import { useToast } from "../toast";
import { KARTEN, KARTEN_IDS } from "../dashboard/cards"; import { KARTEN, KARTEN_IDS, ZEITRAEUME } from "../dashboard/cards";
import Vorschau from "../dashboard/Vorschau"; import Vorschau from "../dashboard/Vorschau";
const Raster = WidthProvider(GridLayout); const Raster = WidthProvider(GridLayout);
@@ -158,9 +158,32 @@ export default function Dashboard() {
} }
} }
/** Nur die Rasterfelder speichern react-grid-layout hängt viel Beiwerk an. */ /** Nur die Rasterfelder speichern react-grid-layout hängt viel Beiwerk an.
* Der gewählte Zeitraum gehört zur Karte und wird mitgespeichert. */
function schlank(liste) { function schlank(liste) {
return (liste || []).map(({ i, x, y, w, h }) => ({ i, x, y, w, h })); return (liste || []).map(({ i, x, y, w, h, tage }) => (
tage ? { i, x, y, w, h, tage } : { i, x, y, w, h }
));
}
/**
* Zeitraum einer Verlaufskarte ändern. Außerhalb des Bearbeitungsmodus wird
* die Wahl still gespeichert sonst wäre sie nach dem nächsten Laden wieder
* weg, ohne dass es einen sichtbaren Speichern-Knopf gäbe.
*/
async function setzeZeitraum(id, tage) {
const geaendert = aktuell.map((k) => (k.i === id ? { ...k, tage } : k));
if (bearbeiten) {
setEntwurf(geaendert);
return;
}
setLayout(geaendert);
if (erzwungen) return;
try {
await api.saveDashboardLayout(schlank(geaendert));
} catch {
/* Nicht speichern zu können ist hier kein Grund, die Ansicht zu stören. */
}
} }
return ( return (
@@ -244,6 +267,17 @@ export default function Dashboard() {
<div className="dash-card-head"> <div className="dash-card-head">
<Icon name={karte.icon} size={16} /> <Icon name={karte.icon} size={16} />
<h2>{karte.titel}</h2> <h2>{karte.titel}</h2>
{/* Zeitraum gehört in den Kopf: So bleibt die volle Kartenhöhe
dem Diagramm. onMouseDown stoppt das Ziehen der Karte. */}
{karte.zeitraum && (
<select className="zeitraum" value={eintrag.tage ?? karte.zeitraum}
onMouseDown={(e) => e.stopPropagation()}
onChange={(e) => setzeZeitraum(eintrag.i, Number(e.target.value))}>
{ZEITRAEUME.map((z) => (
<option key={z.tage} value={z.tage}>{z.label}</option>
))}
</select>
)}
{bearbeiten && ( {bearbeiten && (
<button className="btn-icon danger" title="Karte entfernen" <button className="btn-icon danger" title="Karte entfernen"
onMouseDown={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}
@@ -252,7 +286,9 @@ export default function Dashboard() {
</button> </button>
)} )}
</div> </div>
<div className="dash-card-body"><Inhalt /></div> <div className="dash-card-body">
<Inhalt tage={eintrag.tage ?? karte.zeitraum} />
</div>
</section> </section>
); );
})} })}

View File

@@ -554,9 +554,14 @@ td select { width: auto; min-width: 0; max-width: 100%; }
.karten-titel { font-weight: 600; font-size: 0.875rem; } .karten-titel { font-weight: 600; font-size: 0.875rem; }
/* Auswahlfelder in Karten (Zeitraum, Artikel) */ /* Auswahlfelder in Karten (Zeitraum, Artikel) */
.card-toolbar { display: flex; gap: var(--sp-2); flex-wrap: wrap; } /* Zeitraum-Auswahl sitzt im Kartenkopf klein und zurueckhaltend, damit sie
.card-toolbar select { margin-top: 0; width: auto; min-width: 0; flex: 1 1 140px; } die Ueberschrift nicht ueberlagert. */
select.zeitraum { margin-top: 0; width: auto; min-width: 0; flex: 0 0 auto; align-self: flex-start; } select.zeitraum {
margin-top: 0; width: auto; min-width: 0; flex: 0 0 auto;
padding: 3px 6px; font-size: 0.78rem;
color: var(--muted); border-color: var(--border);
}
select.zeitraum:hover { color: var(--text); }
/* ---------- Startseite: Kartentypen ---------- */ /* ---------- Startseite: Kartentypen ---------- */
.card-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; } .card-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; }