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 +

Lädt…

@@ -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 ` -
-
-
-
- ${elternLabel} - ${_statusBadge(l.status)} +
+ + +
+
+
+
+ ${elternLabel} + ${_statusBadge(l.status)} + ${sichtbarChip} +
+
+ ${datumChip} + ${welpenChip} + ${preisChip} +
-
- ${UI.icon('calendar-dots')} ${_esc(datumLabel)}  ·  - ${UI.icon('dog')} ${verfuegbar}/${gesamt} verfügbar -  ·  ${sichtbarLabel} +
+ + + + + ${_appState.user?.ki_zucht_wurfankuendigung !== 0 ? ` + ` : ''} + +
- ${l.preis_spanne ? `
${UI.icon('currency-eur')} ${_esc(l.preis_spanne)}
` : ''} -
-
- - - - - ${_appState.user?.ki_zucht_wurfankuendigung !== 0 ? ` - ` : ''} - -
+ ${l.beschreibung ? `

${_esc(l.beschreibung)}

` : ''}
- ${l.beschreibung ? `
${_esc(l.beschreibung)}
` : ''} -