Feature: Vollständige Züchter-Rolle — Antrag, Würfe, Stammbaum, Genetik
Basis-Features (Schritte 1–11): - Züchter-Antrag mit Dokument-Upload, Admin-Prüfung, E-Mail-Benachrichtigungen - Öffentliches Züchter-Profil + Karten-Marker (lila, certificate-Icon) - Wurfverwaltung: Würfe, Welpen, Gewichtsverlauf, Foto-System - Wurfbörse (öffentlich) mit Filtersuche nach Rasse/Status - Läufigkeits-Tracker: Deckdatum + Wurftermin (+63 Tage, nur für Züchter) - Interessenten-Chat: Kontakt-Button in Wurfbörse und Züchter-Profil - Sidebar-Einträge: Zuchtkartei + Wurfverwaltung für Züchter/Admin Stammbaum & Genetik (Schritte 1–8): - Zuchtkartei: Hunde-Stammdaten mit Vater/Mutter-Verknüpfung - Stammbaum-Visualisierung: 4 Generationen, horizontales CSS-Grid - Gesundheitstests (HD, ED, OCD, Augen…) mit farbigen Ergebnis-Badges - Genetische Tests (MDR1, PRA, DM…): clear/carrier/affected - Titel & Auszeichnungen (CAC, CACIB, IPO…) - Probeverpaarung: IK-Berechnung nach Wright + Ampel-Bewertung - Teilen-Link für öffentliche Hunde-Profile - Kaufvertrag: druckbares HTML-Dokument pro Welpe Technisch: 4 neue Route-Dateien, 5 neue Page-Module, 11 neue DB-Tabellen, icons shield-check + certificate + tree-structure im Sprite — SW by-v465, APP_VER 444
This commit is contained in:
parent
58cb2b4ad3
commit
91340be5a3
24 changed files with 6660 additions and 27 deletions
|
|
@ -54,6 +54,7 @@ window.Page_map = (() => {
|
|||
parkplatz: [],
|
||||
treffpunkt: [],
|
||||
community: [],
|
||||
zuechter: [],
|
||||
};
|
||||
|
||||
const VISIBLE_KEY = 'by_map_visible_v1';
|
||||
|
|
@ -89,6 +90,7 @@ window.Page_map = (() => {
|
|||
parkplatz: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#car"></use></svg>', label: 'Parkplatz', color: '#2563EB', z: 5 },
|
||||
treffpunkt: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#handshake"></use></svg>', label: 'Treffpunkt', color: '#7C3AED', z: 25 },
|
||||
community: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#push-pin"></use></svg>', label: 'Sonstiges', color: '#F59E0B', z: 30 },
|
||||
zuechter: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#certificate"></use></svg>', label: 'Züchter', color: '#7C3AED', z: 50 },
|
||||
};
|
||||
|
||||
// Frontend-Layer → Backend-Typ Mapping
|
||||
|
|
@ -998,13 +1000,15 @@ window.Page_map = (() => {
|
|||
(_layers.poison || []).forEach(m => m._dangerCircle?.remove());
|
||||
Object.keys(_layers).forEach(k => { _layers[k] = []; });
|
||||
|
||||
const [places, poisonList] = await Promise.allSettled([
|
||||
const [places, poisonList, breederList] = await Promise.allSettled([
|
||||
API.places.list(),
|
||||
_userPos ? API.poison.listNearby(_userPos.lat, _userPos.lon, 10000) : Promise.resolve([]),
|
||||
API.breeder.mapMarkers(),
|
||||
]);
|
||||
|
||||
if (places.status === 'fulfilled') _addPlaces(places.value);
|
||||
if (poisonList.status === 'fulfilled') _addPoison(poisonList.value);
|
||||
if (breederList.status === 'fulfilled') _addBreeders(breederList.value);
|
||||
_scheduleOsmLoad();
|
||||
}
|
||||
|
||||
|
|
@ -1039,6 +1043,59 @@ window.Page_map = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
function _addBreeders(breeders) {
|
||||
if (!_map || !window.L) return;
|
||||
const t = TYPEN.zuechter;
|
||||
const cluster = _getCluster('zuechter');
|
||||
const markers = [];
|
||||
|
||||
breeders.forEach(b => {
|
||||
// Ohne Koordinaten: stillen Skip
|
||||
if (b.location_lat == null || b.location_lng == null) return;
|
||||
|
||||
const icon = L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="background:${t.color};color:#fff;font-size:15px;
|
||||
width:32px;height:32px;border-radius:50%;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
box-shadow:0 2px 5px rgba(0,0,0,0.35);
|
||||
border:2px solid rgba(255,255,255,0.7)">${t.icon}</div>`,
|
||||
iconSize: [32, 32], iconAnchor: [16, 16],
|
||||
});
|
||||
|
||||
const marker = L.marker([b.location_lat, b.location_lng], { icon, zIndexOffset: t.z ?? 0 })
|
||||
.bindTooltip(_esc(b.zwingername), { direction: 'top', offset: [0, -16] });
|
||||
|
||||
marker.on('click', () => {
|
||||
const rasseText = b.rasse_text ? `<div style="font-size:12px;color:#666;margin-bottom:4px">${_esc(b.rasse_text)}</div>` : '';
|
||||
const stadtText = b.stadt ? `<div style="font-size:12px;color:#888;margin-bottom:8px">${_esc(b.stadt)}</div>` : '';
|
||||
|
||||
marker.bindPopup(`
|
||||
<div style="min-width:170px;max-width:240px">
|
||||
<div style="font-weight:600;margin-bottom:6px">${t.icon} ${_esc(b.zwingername)}</div>
|
||||
${rasseText}${stadtText}
|
||||
<button class="btn btn-primary btn-sm" id="breeder-profile-btn">Profil ansehen</button>
|
||||
</div>
|
||||
`, { maxWidth: 260 }).openPopup();
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('breeder-profile-btn')?.addEventListener('click', () => {
|
||||
marker.closePopup();
|
||||
App.navigate('breeder', true, { zwingername: b.zwingername });
|
||||
});
|
||||
}, 50);
|
||||
});
|
||||
|
||||
markers.push(marker);
|
||||
_layers.zuechter.push(marker);
|
||||
});
|
||||
|
||||
cluster.addLayers(markers);
|
||||
if (_visible.zuechter !== false && _map && !_map.hasLayer(cluster)) {
|
||||
cluster.addTo(_map);
|
||||
}
|
||||
}
|
||||
|
||||
function _createSimpleMarker(lat, lon, t, tooltip, onClick) {
|
||||
const icon = L.divIcon({
|
||||
className: '',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue