diff --git a/backend/main.py b/backend/main.py
index 6d96fc8..5b1f761 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -404,7 +404,7 @@ async def serve_media(path: str, request: _Request):
raise _HE(404, "Nicht gefunden.")
return _media_response(filepath)
-APP_VER = "892" # muss mit APP_VER in app.js übereinstimmen
+APP_VER = "893" # muss mit APP_VER in app.js übereinstimmen
@app.get("/.well-known/assetlinks.json")
async def assetlinks():
diff --git a/backend/static/index.html b/backend/static/index.html
index 5bbd100..cc05416 100644
--- a/backend/static/index.html
+++ b/backend/static/index.html
@@ -583,10 +583,10 @@
-
-
-
-
+
+
+
+
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index def8158..2c1b582 100644
--- a/backend/static/js/app.js
+++ b/backend/static/js/app.js
@@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
-const APP_VER = '892'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '893'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app';
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
diff --git a/backend/static/js/pages/litters.js b/backend/static/js/pages/litters.js
index d1f42d7..12ced25 100644
--- a/backend/static/js/pages/litters.js
+++ b/backend/static/js/pages/litters.js
@@ -100,6 +100,7 @@ window.Page_litters = (() => {
${UI.icon('plus')} Neuer Wurf
+
@@ -132,6 +133,33 @@ window.Page_litters = (() => {
// ----------------------------------------------------------
// Würfe-Liste rendern
// ----------------------------------------------------------
+ function _renderStats() {
+ const bar = document.getElementById('litters-stats');
+ if (!bar || !_litters.length) return;
+ const total = _litters.length;
+ const aktiv = _litters.filter(l => l.status === 'verfuegbar' || l.status === 'geboren').length;
+ const geplant = _litters.filter(l => l.status === 'geplant').length;
+ const welpen = _litters.reduce((s, l) => s + (l.welpen_gesamt || 0), 0);
+ const verfuegb = _litters.reduce((s, l) => s + (l.welpen_verfuegbar || 0), 0);
+ const statItems = [
+ { icon: 'list-bullets', label: 'Würfe gesamt', val: total },
+ { icon: 'baby', label: 'Aktiv', val: aktiv, color: 'var(--c-success)' },
+ { icon: 'calendar-dots',label: 'Geplant', val: geplant },
+ { icon: 'dog', label: 'Welpen', val: welpen },
+ { icon: 'tag', label: 'Verfügbar', val: verfuegb, color: verfuegb > 0 ? 'var(--c-primary)' : undefined },
+ ];
+ bar.style.display = 'flex';
+ bar.innerHTML = statItems.map(s => `
+
+
${UI.icon(s.icon)}
+
+
${s.val}
+
${s.label}
+
+
`).join('');
+ }
+
function _renderList() {
const el = document.getElementById('litters-list');
if (!el) return;
@@ -149,6 +177,7 @@ window.Page_litters = (() => {
return;
}
+ _renderStats();
el.innerHTML = _litters.map(l => _litterCardHTML(l)).join('');
// Events
@@ -218,74 +247,96 @@ window.Page_litters = (() => {
if (_openId) _togglePuppies(_openId, true);
}
+ function _daysUntil(dateStr) {
+ if (!dateStr) return null;
+ const diff = Math.ceil((new Date(dateStr) - new Date()) / 86400000);
+ return diff;
+ }
+
function _litterCardHTML(l) {
- const verfuegbar = l.welpen_verfuegbar != null ? l.welpen_verfuegbar : '?';
- const gesamt = l.welpen_gesamt != null ? l.welpen_gesamt : '?';
- const datumLabel = l.geburt_datum
- ? `Geburt: ${_fmtDate(l.geburt_datum)}`
- : l.erwartetes_datum
- ? `Erwartet: ${_fmtDate(l.erwartetes_datum)}`
- : '—';
+ const verfuegbar = l.welpen_verfuegbar != null ? l.welpen_verfuegbar : '?';
+ const gesamt = l.welpen_gesamt != null ? l.welpen_gesamt : '?';
+ const elternLabel = [l.vater_name, l.mutter_name].filter(Boolean).map(n => _esc(n)).join(' × ') || '—';
- const elternLabel = [l.vater_name, l.mutter_name]
- .filter(Boolean)
- .map(n => _esc(n))
- .join(' × ') || '—';
+ // Datum + Countdown
+ let datumChip = '';
+ const refDate = l.geburt_datum || l.erwartetes_datum;
+ if (refDate) {
+ const days = _daysUntil(refDate);
+ const label = l.geburt_datum ? `Geburt ${_fmtDate(l.geburt_datum)}` : `Erwartet ${_fmtDate(l.erwartetes_datum)}`;
+ let countdownHtml = '';
+ if (days !== null && !l.geburt_datum) {
+ const c = days < 0 ? `überfällig`
+ : days === 0 ? `heute!`
+ : days <= 7 ? `${days}d`
+ : `${days}d`;
+ countdownHtml = ` · ${c}`;
+ }
+ datumChip = `${UI.icon('calendar-dots')} ${label}${countdownHtml}`;
+ }
- const sichtbarLabel = l.sichtbar
- ? `${UI.icon('eye')} Öffentlich`
- : `${UI.icon('eye-slash')} Nicht öffentlich`;
+ const sichtbarChip = l.sichtbar
+ ? `${UI.icon('eye')} Öffentlich`
+ : `${UI.icon('eye-slash')} Nicht öffentlich`;
+
+ const welpenChip = `${UI.icon('dog')} ${verfuegbar}/${gesamt} verfügbar`;
+
+ const preisChip = l.preis_spanne
+ ? `${UI.icon('currency-eur')} ${_esc(l.preis_spanne)}`
+ : '';
return `
-
-