Bündel 2: Zentrale Helper für DRY-Cleanup, SW by-v1114
NEUE BACKEND-MODULE:
math_utils.py
- haversine_km(lat1, lon1, lat2, lon2) — Distanz in km
- haversine_m(...) — Convenience-Wrapper in Metern
- bbox_deg_from_km(lat, radius_km) — Bounding-Box-Approximation
für SQL-Vorfilter (statt Haversine im Python-Loop)
config.py
- DB_PATH, MEDIA_DIR, BREEDER_DOCS_DIR, SCANINPUT_DIR
- API_TIMEOUT_SHORT (5s) / DEFAULT (10s) / LONG (30s)
- HTTP_USER_AGENT, HTTP_HEADERS
errors.py
- not_found(msg), forbidden(msg), bad_request(msg), unauthorized(msg)
- conflict(msg), too_many_requests(msg, retry_after), service_unavailable(msg)
- require_or_404(row, msg) — Convenience-Helper
UI.JS ERWEITERUNGEN:
UI.time erweitert:
- formatDate(d) → "15.03.2026"
- formatDateTime(d) → "15.03.2026, 14:30"
- weekday(d) → "Di"
- parseISO(str) → {year, month, day}
UI.text (neu):
- truncate(str, maxLen, ellipsis='…')
- slug(str) — URL-Slug aus String (mit DE-Umlauten)
UI.money (neu):
- format(value) → "12,34 €" (de-DE, EUR)
- formatWithSuffix(value, '/Jahr')
HAVERSINE-MIGRATION (13 Backend-Routen):
alerts.py, services.py, places.py, events.py, diary.py, playdate.py,
lost.py, poison.py, adoption.py, gassi_zeiten.py, sitting.py, routen.py,
walks.py
- Alle lokalen def _haversine/haversine_km entfernt
- Aufrufe ersetzt durch haversine_km/haversine_m je nach Einheit
- from math_utils import haversine_km|haversine_m in jeder Datei
Tests 19/19 grün.
Hinweis: Migrationen für MEDIA_DIR (19 Stellen), API-Timeouts (12),
Date-Formatter im Frontend (24) und UI.text.truncate (5) sind als
Folge-Sprints möglich. Helper sind verfügbar.
This commit is contained in:
parent
c517c9281d
commit
297bd22f96
22 changed files with 225 additions and 202 deletions
|
|
@ -490,13 +490,73 @@ const UI = (() => {
|
|||
return fmtDate.format(new Date(dateStr));
|
||||
}
|
||||
|
||||
// Datum: "15.03.2026" / "15.03.2026, 14:30"
|
||||
const _fmtDateNumeric = new Intl.DateTimeFormat('de-DE', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric'
|
||||
});
|
||||
const _fmtDateTimeNumeric = new Intl.DateTimeFormat('de-DE', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
// Wochentag: "Di." / Tag im Monat: "15"
|
||||
const _fmtWeekday = new Intl.DateTimeFormat('de-DE', { weekday: 'short' });
|
||||
|
||||
return {
|
||||
relative,
|
||||
format: d => fmtDate.format(new Date(d)),
|
||||
formatShort: d => fmtDateShort.format(new Date(d)),
|
||||
format: d => fmtDate.format(new Date(d)),
|
||||
formatShort: d => fmtDateShort.format(new Date(d)),
|
||||
formatDate: d => _fmtDateNumeric.format(new Date(d)), // 15.03.2026
|
||||
formatDateTime:d => _fmtDateTimeNumeric.format(new Date(d)), // 15.03.2026, 14:30
|
||||
weekday: d => _fmtWeekday.format(new Date(d)).replace('.', ''),
|
||||
// ISO-Parser: "2026-03-15" → { year, month, day }
|
||||
parseISO(str) {
|
||||
if (!str) return null;
|
||||
const m = String(str).match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
return m ? { year: +m[1], month: +m[2], day: +m[3] } : null;
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// TEXT — String-Helper
|
||||
// ----------------------------------------------------------
|
||||
const text = {
|
||||
/** Schneidet str auf maxLen ab und hängt ellipsis an. */
|
||||
truncate(str, maxLen = 80, ellipsis = '…') {
|
||||
if (!str) return '';
|
||||
const s = String(str);
|
||||
return s.length <= maxLen ? s : s.slice(0, maxLen - ellipsis.length) + ellipsis;
|
||||
},
|
||||
/** Slug aus String — für URL-Pfade. */
|
||||
slug(str) {
|
||||
return String(str || '')
|
||||
.toLowerCase()
|
||||
.replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss')
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// MONEY — Currency-Formatierung (de-DE, EUR)
|
||||
// ----------------------------------------------------------
|
||||
const _fmtEur = new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency', currency: 'EUR',
|
||||
minimumFractionDigits: 2, maximumFractionDigits: 2,
|
||||
});
|
||||
const money = {
|
||||
/** Formatiert Zahl als "12,34 €" — null/undefined ergibt "—". */
|
||||
format(value) {
|
||||
if (value == null || value === '' || isNaN(value)) return '—';
|
||||
return _fmtEur.format(Number(value));
|
||||
},
|
||||
/** Mit Suffix wie "/Jahr". */
|
||||
formatWithSuffix(value, suffix = '') {
|
||||
if (value == null || value === '' || isNaN(value)) return '—';
|
||||
return _fmtEur.format(Number(value)) + (suffix ? ' ' + suffix : '');
|
||||
},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// FOTO-VORSCHAU (Input[type=file] → img)
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -1272,7 +1332,7 @@ const UI = (() => {
|
|||
toast, modal,
|
||||
setLoading, asyncButton,
|
||||
formData, setFormError, clearFormErrors,
|
||||
emptyState, errorState, time,
|
||||
emptyState, errorState, time, text, money,
|
||||
previewUrl, previewFallback,
|
||||
setupPhotoPreview, scrollTop, skeleton, skeletonList,
|
||||
moneyInput, parseMoney, datePicker,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue