Feature: Aktive Erinnerungen, Versicherung, Verhaltensprotokoll, Hundefreundliche Orte (SW by-v874)
This commit is contained in:
parent
83034c0db0
commit
b818f85f36
11 changed files with 589 additions and 14 deletions
|
|
@ -219,6 +219,14 @@ const API = (() => {
|
|||
gewichtVerlauf(dogId) {
|
||||
return get(`/dogs/${dogId}/health/gewicht`);
|
||||
},
|
||||
reminders(dogId) { return get(`/dogs/${dogId}/reminders`); },
|
||||
insuranceList(dogId) { return get(`/dogs/${dogId}/insurance`); },
|
||||
insuranceCreate(dogId, d) { return post(`/dogs/${dogId}/insurance`, d); },
|
||||
insuranceUpdate(dogId, id, d) { return patch(`/dogs/${dogId}/insurance/${id}`, d); },
|
||||
insuranceDelete(dogId, id) { return del(`/dogs/${dogId}/insurance/${id}`); },
|
||||
behaviorList(dogId) { return get(`/dogs/${dogId}/behavior`); },
|
||||
behaviorCreate(dogId, d) { return post(`/dogs/${dogId}/behavior`, d); },
|
||||
behaviorDelete(dogId, id) { return del(`/dogs/${dogId}/behavior/${id}`); },
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '873'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '874'; // ← 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
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ window.Page_health = (() => {
|
|||
{ key: 'allergie', label: 'Allergien', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#leaf"></use></svg>' },
|
||||
{ key: 'dokument', label: 'Dokumente', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg>' },
|
||||
{ key: 'praxen', label: 'Praxen', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' },
|
||||
{ key: 'versicherung', label: 'Versicherung', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#shield-check"></use></svg>' },
|
||||
{ key: 'verhalten', label: 'Verhalten', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#brain"></use></svg>' },
|
||||
];
|
||||
const LAEUFIGKEIT_TAB = { key: 'laeufigkeit', label: 'Läufigkeit', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#gender-female"></use></svg>' };
|
||||
|
||||
|
|
@ -111,12 +113,14 @@ window.Page_health = (() => {
|
|||
<div id="health-ki-berichte"></div>
|
||||
<div id="health-terminvorschlaege"></div>
|
||||
<div id="health-reminders"></div>
|
||||
<div id="health-reminders-banner" style="display:none;padding:0 0 var(--space-2);display:flex;flex-direction:column;gap:var(--space-1)"></div>
|
||||
<div class="by-tabs" id="by-tabs"></div>
|
||||
<div id="by-tab-content"></div>
|
||||
`;
|
||||
|
||||
_renderTabBar();
|
||||
UI.bindDogChip(_container, _appState);
|
||||
_loadRemindersBanner();
|
||||
_container.querySelector('#health-ki-btn')
|
||||
.addEventListener('click', _showKiSummary);
|
||||
_container.querySelector('#health-ki-tierarzt-btn')
|
||||
|
|
@ -332,6 +336,8 @@ window.Page_health = (() => {
|
|||
case 'allergie': content.innerHTML = _renderAllergien(entries); break;
|
||||
case 'dokument': content.innerHTML = _renderDokumente(entries); break;
|
||||
case 'praxen': content.innerHTML = _renderPraxen(); break;
|
||||
case 'versicherung': _renderVersicherung(content); break;
|
||||
case 'verhalten': _renderVerhalten(content); break;
|
||||
}
|
||||
|
||||
_bindTabEvents(content);
|
||||
|
|
@ -3050,6 +3056,331 @@ window.Page_health = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// BEVORSTEHENDE ERINNERUNGEN (Banner oben in der Health-Seite)
|
||||
// ==============================================================
|
||||
async function _loadRemindersBanner() {
|
||||
const dog = _appState?.activeDog;
|
||||
if (!dog) return;
|
||||
const wrap = _container?.querySelector('#health-reminders-banner');
|
||||
if (!wrap) return;
|
||||
let items;
|
||||
try { items = await API.health.reminders(dog.id); }
|
||||
catch { return; }
|
||||
if (!items.length) { wrap.style.display = 'none'; return; }
|
||||
|
||||
const TYPE_LABEL = { impfung: 'Impfung', entwurmung: 'Entwurmung', medikament: 'Medikament' };
|
||||
const fmt = d => { try { const p = d.split('-'); return `${parseInt(p[2])}.${parseInt(p[1])}.${p[0]}`; } catch { return d; } };
|
||||
|
||||
wrap.style.display = '';
|
||||
wrap.innerHTML = items.slice(0, 3).map(r => {
|
||||
const overdue = r.ueberfaellig;
|
||||
const color = overdue ? 'var(--c-danger,#ef4444)' : r.delta_tage <= 3 ? '#f59e0b' : 'var(--c-primary)';
|
||||
const bg = overdue ? 'rgba(239,68,68,0.08)' : r.delta_tage <= 3 ? 'rgba(245,158,11,0.08)' : 'var(--c-primary-subtle)';
|
||||
const label = overdue ? `Überfällig seit ${Math.abs(r.delta_tage)} Tag${Math.abs(r.delta_tage)!==1?'en':''}` :
|
||||
r.delta_tage === 0 ? 'Heute fällig' :
|
||||
`in ${r.delta_tage} Tag${r.delta_tage!==1?'en':''}`;
|
||||
return `
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);
|
||||
background:${bg};border-radius:var(--radius-md);border-left:3px solid ${color}">
|
||||
<svg class="ph-icon" style="width:16px;height:16px;color:${color};flex-shrink:0" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#bell-ringing"></use>
|
||||
</svg>
|
||||
<div style="flex:1;min-width:0">
|
||||
<span style="font-weight:600;font-size:var(--text-sm);color:var(--c-text)">${_esc(r.bezeichnung)}</span>
|
||||
<span style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-left:var(--space-1)">${TYPE_LABEL[r.typ] || r.typ}</span>
|
||||
</div>
|
||||
<span style="font-size:var(--text-xs);font-weight:600;color:${color};white-space:nowrap">${label}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// TAB: VERSICHERUNG
|
||||
// ==============================================================
|
||||
async function _renderVersicherung(content) {
|
||||
const dog = _appState?.activeDog;
|
||||
if (!dog) return;
|
||||
content.innerHTML = `<div style="padding:var(--space-4) 0">
|
||||
<div style="text-align:center;color:var(--c-text-muted);padding:var(--space-4)">
|
||||
<svg class="ph-icon" style="width:24px;height:24px;animation:spin 1s linear infinite" aria-hidden="true"><use href="/icons/phosphor.svg#spinner-gap"></use></svg>
|
||||
</div></div>`;
|
||||
|
||||
let policies;
|
||||
try { policies = await API.health.insuranceList(dog.id); }
|
||||
catch { content.innerHTML = `<p style="color:var(--c-danger);padding:var(--space-4)">Fehler beim Laden.</p>`; return; }
|
||||
|
||||
const _fmtDate = d => { if (!d) return '–'; try { const p=d.split('-'); return `${parseInt(p[2])}.${parseInt(p[1])}.${p[0]}`; } catch { return d; } };
|
||||
const _fmtEur = v => v ? `${v.toFixed(2).replace('.',',')} €/Jahr` : '–';
|
||||
|
||||
const cardsHtml = policies.length ? policies.map(p => `
|
||||
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-3)" data-ins-id="${p.id}">
|
||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-2)">
|
||||
<div>
|
||||
<div style="font-weight:700;font-size:var(--text-base)">${_esc(p.anbieter)}</div>
|
||||
${p.police_nr ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">Police: ${_esc(p.police_nr)}</div>` : ''}
|
||||
</div>
|
||||
<div style="display:flex;gap:var(--space-1)">
|
||||
<button class="btn btn-ghost btn-sm ins-edit-btn" data-id="${p.id}" style="padding:4px 8px">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#pencil"></use></svg>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm ins-del-btn" data-id="${p.id}" style="padding:4px 8px;color:var(--c-danger)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2);margin-top:var(--space-3);font-size:var(--text-sm)">
|
||||
<div><span style="color:var(--c-text-secondary)">Jahresbeitrag</span><br><strong>${_fmtEur(p.jahresbeitrag)}</strong></div>
|
||||
<div><span style="color:var(--c-text-secondary)">Läuft ab</span><br><strong>${_fmtDate(p.ablaufdatum)}</strong></div>
|
||||
${p.kontakt ? `<div style="grid-column:1/-1"><span style="color:var(--c-text-secondary)">Kontakt</span><br>${_esc(p.kontakt)}</div>` : ''}
|
||||
${p.notizen ? `<div style="grid-column:1/-1"><span style="color:var(--c-text-secondary)">Notizen</span><br>${_esc(p.notizen)}</div>` : ''}
|
||||
</div>
|
||||
</div>`).join('') : `
|
||||
<div style="text-align:center;padding:var(--space-6);color:var(--c-text-muted)">
|
||||
<svg class="ph-icon" style="width:2.5rem;height:2.5rem;margin-bottom:var(--space-3);display:block;margin-inline:auto" aria-hidden="true"><use href="/icons/phosphor.svg#shield-check"></use></svg>
|
||||
<div style="font-size:var(--text-sm)">Noch keine Versicherung eingetragen.</div>
|
||||
</div>`;
|
||||
|
||||
content.innerHTML = `<div style="padding:var(--space-4) 0">
|
||||
${cardsHtml}
|
||||
<button class="btn btn-primary" id="ins-add-btn" style="width:100%;margin-top:var(--space-2)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#plus"></use></svg>
|
||||
Versicherung eintragen
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
content.querySelector('#ins-add-btn')?.addEventListener('click', () => _openInsuranceForm(dog, null, () => _renderVersicherung(content)));
|
||||
content.querySelectorAll('.ins-edit-btn').forEach(btn => {
|
||||
const pol = policies.find(p => p.id === parseInt(btn.dataset.id));
|
||||
btn.addEventListener('click', () => _openInsuranceForm(dog, pol, () => _renderVersicherung(content)));
|
||||
});
|
||||
content.querySelectorAll('.ins-del-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!window.confirm('Versicherung löschen?')) return;
|
||||
await API.health.insuranceDelete(dog.id, parseInt(btn.dataset.id));
|
||||
_renderVersicherung(content);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _openInsuranceForm(dog, existing, onSave) {
|
||||
const id = `ins-form-${Date.now()}`;
|
||||
const body = `<form id="${id}">
|
||||
<div class="by-form-group"><label class="by-label">Anbieter *</label>
|
||||
<input type="text" name="anbieter" class="form-control by-input" value="${_esc(existing?.anbieter||'')}" required placeholder="z. B. HUK-Coburg">
|
||||
</div>
|
||||
<div class="by-form-group"><label class="by-label">Police-Nr.</label>
|
||||
<input type="text" name="police_nr" class="form-control by-input" value="${_esc(existing?.police_nr||'')}" placeholder="optional">
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||||
<div class="by-form-group"><label class="by-label">Jahresbeitrag (€)</label>
|
||||
<input type="number" name="jahresbeitrag" class="form-control by-input" value="${existing?.jahresbeitrag||''}" min="0" step="0.01" placeholder="z. B. 149.00">
|
||||
</div>
|
||||
<div class="by-form-group"><label class="by-label">Ablaufdatum</label>
|
||||
<input type="date" name="ablaufdatum" class="form-control by-input" value="${existing?.ablaufdatum||''}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="by-form-group"><label class="by-label">Kontakt / Telefon</label>
|
||||
<input type="text" name="kontakt" class="form-control by-input" value="${_esc(existing?.kontakt||'')}" placeholder="optional">
|
||||
</div>
|
||||
<div class="by-form-group"><label class="by-label">Notizen</label>
|
||||
<textarea name="notizen" class="form-control by-input" rows="2">${_esc(existing?.notizen||'')}</textarea>
|
||||
</div>
|
||||
</form>`;
|
||||
const footer = `
|
||||
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
|
||||
<button class="btn btn-primary" id="ins-save-btn" form="${id}">Speichern</button>`;
|
||||
UI.modal.open({ title: existing ? 'Versicherung bearbeiten' : 'Versicherung eintragen', body, footer });
|
||||
setTimeout(() => {
|
||||
document.getElementById('ins-save-btn')?.addEventListener('click', async ev => {
|
||||
ev.preventDefault();
|
||||
const form = document.getElementById(id);
|
||||
if (!form) return;
|
||||
const fd = new FormData(form);
|
||||
const data = {
|
||||
anbieter: (fd.get('anbieter')||'').trim(),
|
||||
police_nr: fd.get('police_nr')||null,
|
||||
jahresbeitrag: fd.get('jahresbeitrag') ? parseFloat(fd.get('jahresbeitrag')) : null,
|
||||
ablaufdatum: fd.get('ablaufdatum')||null,
|
||||
kontakt: fd.get('kontakt')||null,
|
||||
notizen: fd.get('notizen')||null,
|
||||
};
|
||||
if (!data.anbieter) { UI.toast.warning('Bitte Anbieter angeben.'); return; }
|
||||
await UI.asyncButton(document.getElementById('ins-save-btn'), async () => {
|
||||
try {
|
||||
if (existing) await API.health.insuranceUpdate(dog.id, existing.id, data);
|
||||
else await API.health.insuranceCreate(dog.id, data);
|
||||
UI.modal.close();
|
||||
UI.toast.success('Gespeichert.');
|
||||
onSave();
|
||||
} catch (err) { UI.toast.error(err.message || 'Fehler.'); }
|
||||
});
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// TAB: VERHALTEN
|
||||
// ==============================================================
|
||||
const _KAT_LABELS = {
|
||||
angst: 'Angst / Panik', aggression: 'Aggression', ueberreaktion: 'Überreaktion',
|
||||
ressource: 'Ressourcenverteidigung', separation: 'Trennungsangst',
|
||||
leine: 'Leinenprobleme', sozial: 'Sozialkompetenz', sonstiges: 'Sonstiges',
|
||||
};
|
||||
const _KAT_COLORS = {
|
||||
angst: '#3b82f6', aggression: '#ef4444', ueberreaktion: '#f59e0b',
|
||||
ressource: '#8b5cf6', separation: '#ec4899', leine: '#06b6d4',
|
||||
sozial: '#22c55e', sonstiges: '#6b7280',
|
||||
};
|
||||
const _TRIGGER_LABELS = {
|
||||
fremde_hunde: 'Fremde Hunde', fremde_menschen: 'Fremde Menschen', kinder: 'Kinder',
|
||||
laerm_feuerwerk: 'Feuerwerk', laerm_gewitter: 'Gewitter', auto_fahrrad: 'Autos/Fahrräder',
|
||||
tierarzt: 'Tierarztbesuch', allein_zuhause: 'Allein zuhause',
|
||||
andere_tiere: 'Andere Tiere', besucher_zuhause: 'Besucher', sonstiges: 'Sonstiges',
|
||||
};
|
||||
|
||||
async function _renderVerhalten(content) {
|
||||
const dog = _appState?.activeDog;
|
||||
if (!dog) return;
|
||||
content.innerHTML = `<div style="padding:var(--space-4) 0">
|
||||
<div style="text-align:center;color:var(--c-text-muted);padding:var(--space-4)">
|
||||
<svg class="ph-icon" style="width:24px;height:24px;animation:spin 1s linear infinite" aria-hidden="true"><use href="/icons/phosphor.svg#spinner-gap"></use></svg>
|
||||
</div></div>`;
|
||||
|
||||
let resp;
|
||||
try { resp = await API.health.behaviorList(dog.id); }
|
||||
catch { content.innerHTML = `<p style="color:var(--c-danger);padding:var(--space-4)">Fehler beim Laden.</p>`; return; }
|
||||
|
||||
const entries = resp.entries || [];
|
||||
const fmtDate = d => { try { const p=d.split('-'); return `${parseInt(p[2])}.${parseInt(p[1])}.${p[0]}`; } catch { return d; } };
|
||||
|
||||
const listHtml = entries.length ? entries.map(e => {
|
||||
const color = _KAT_COLORS[e.kategorie] || '#6b7280';
|
||||
const katLabel = _KAT_LABELS[e.kategorie] || e.kategorie;
|
||||
const trigLabel = _TRIGGER_LABELS[e.trigger] || e.trigger || '';
|
||||
const dots = Array.from({length: 5}, (_,i) =>
|
||||
`<div style="width:8px;height:8px;border-radius:50%;background:${i < e.intensitaet ? color : 'var(--c-border)'}"></div>`
|
||||
).join('');
|
||||
return `
|
||||
<div class="card" style="padding:var(--space-3);margin-bottom:var(--space-2);display:flex;align-items:flex-start;gap:var(--space-3)">
|
||||
<div style="width:3px;border-radius:2px;background:${color};align-self:stretch;flex-shrink:0"></div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
|
||||
<span style="font-weight:700;font-size:var(--text-sm);color:${color}">${_esc(katLabel)}</span>
|
||||
${trigLabel ? `<span style="font-size:var(--text-xs);background:var(--c-surface-2);padding:1px 6px;border-radius:100px;color:var(--c-text-secondary)">${_esc(trigLabel)}</span>` : ''}
|
||||
<span style="font-size:var(--text-xs);color:var(--c-text-muted);margin-left:auto">${fmtDate(e.datum)}${e.uhrzeit ? ' ' + e.uhrzeit : ''}</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:3px;margin-top:4px">${dots}</div>
|
||||
${e.notiz ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:4px">${_esc(e.notiz)}</div>` : ''}
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm beh-del-btn" data-id="${e.id}" style="padding:4px 6px;color:var(--c-danger);flex-shrink:0">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>
|
||||
</button>
|
||||
</div>`;
|
||||
}).join('') : `
|
||||
<div style="text-align:center;padding:var(--space-6);color:var(--c-text-muted)">
|
||||
<svg class="ph-icon" style="width:2.5rem;height:2.5rem;margin-bottom:var(--space-3);display:block;margin-inline:auto" aria-hidden="true"><use href="/icons/phosphor.svg#brain"></use></svg>
|
||||
<div style="font-size:var(--text-sm)">Noch keine Einträge. Protokolliere auffälliges Verhalten um Muster zu erkennen.</div>
|
||||
</div>`;
|
||||
|
||||
content.innerHTML = `<div style="padding:var(--space-4) 0">
|
||||
${listHtml}
|
||||
<button class="btn btn-primary" id="beh-add-btn" style="width:100%;margin-top:var(--space-2)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#plus"></use></svg>
|
||||
Verhalten erfassen
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
content.querySelector('#beh-add-btn')?.addEventListener('click', () => _openBehaviorForm(dog, () => _renderVerhalten(content)));
|
||||
content.querySelectorAll('.beh-del-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!window.confirm('Eintrag löschen?')) return;
|
||||
await API.health.behaviorDelete(dog.id, parseInt(btn.dataset.id));
|
||||
_renderVerhalten(content);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _openBehaviorForm(dog, onSave) {
|
||||
const id = `beh-form-${Date.now()}`;
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const nowTime = (() => { const d=new Date(); return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; })();
|
||||
const body = `<form id="${id}">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||||
<div class="by-form-group"><label class="by-label">Datum</label>
|
||||
<input type="date" name="datum" class="form-control by-input" value="${today}" required>
|
||||
</div>
|
||||
<div class="by-form-group"><label class="by-label">Uhrzeit</label>
|
||||
<input type="time" name="uhrzeit" class="form-control by-input" value="${nowTime}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="by-form-group"><label class="by-label">Kategorie *</label>
|
||||
<select name="kategorie" class="form-control by-select" required>
|
||||
<option value="">– wählen –</option>
|
||||
${Object.entries(_KAT_LABELS).map(([k,v]) => `<option value="${k}">${v}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="by-form-group"><label class="by-label">Intensität (1 = gering, 5 = stark)</label>
|
||||
<div style="display:flex;gap:var(--space-2)">
|
||||
${[1,2,3,4,5].map(n => `<button type="button" class="beh-int-btn" data-val="${n}"
|
||||
style="flex:1;padding:10px;border-radius:8px;border:1.5px solid var(--c-border);
|
||||
background:${n<=3?'var(--c-primary)':'var(--c-bg-card)'};
|
||||
color:${n<=3?'#fff':'var(--c-text-secondary)'};font-weight:700;cursor:pointer">${n}</button>`).join('')}
|
||||
</div>
|
||||
<input type="hidden" name="intensitaet" value="3">
|
||||
</div>
|
||||
<div class="by-form-group"><label class="by-label">Auslöser</label>
|
||||
<select name="trigger" class="form-control by-select">
|
||||
<option value="">– unbekannt –</option>
|
||||
${Object.entries(_TRIGGER_LABELS).map(([k,v]) => `<option value="${k}">${v}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="by-form-group"><label class="by-label">Notiz</label>
|
||||
<textarea name="notiz" class="form-control by-input" rows="2" placeholder="Was ist passiert?"></textarea>
|
||||
</div>
|
||||
</form>`;
|
||||
const footer = `
|
||||
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
|
||||
<button class="btn btn-primary" id="beh-save-btn" form="${id}">Speichern</button>`;
|
||||
UI.modal.open({ title: 'Verhalten erfassen', body, footer });
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('.beh-int-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const val = parseInt(btn.dataset.val);
|
||||
document.querySelectorAll('.beh-int-btn').forEach((b,i) => {
|
||||
b.style.background = i < val ? 'var(--c-primary)' : 'var(--c-bg-card)';
|
||||
b.style.color = i < val ? '#fff' : 'var(--c-text-secondary)';
|
||||
});
|
||||
const hi = document.querySelector('[name="intensitaet"]');
|
||||
if (hi) hi.value = val;
|
||||
});
|
||||
});
|
||||
document.getElementById('beh-save-btn')?.addEventListener('click', async ev => {
|
||||
ev.preventDefault();
|
||||
const form = document.getElementById(id);
|
||||
if (!form) return;
|
||||
const fd = new FormData(form);
|
||||
const data = {
|
||||
datum: fd.get('datum'),
|
||||
uhrzeit: fd.get('uhrzeit')||null,
|
||||
kategorie: fd.get('kategorie'),
|
||||
intensitaet: parseInt(fd.get('intensitaet')||'3'),
|
||||
trigger: fd.get('trigger')||null,
|
||||
notiz: (fd.get('notiz')||'').trim()||null,
|
||||
};
|
||||
if (!data.kategorie) { UI.toast.warning('Bitte Kategorie wählen.'); return; }
|
||||
await UI.asyncButton(document.getElementById('beh-save-btn'), async () => {
|
||||
try {
|
||||
await API.health.behaviorCreate(dog.id, data);
|
||||
UI.modal.close();
|
||||
UI.toast.success('Eintrag gespeichert.');
|
||||
onSave();
|
||||
} catch (err) { UI.toast.error(err.message || 'Fehler.'); }
|
||||
});
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
|
||||
return { init, refresh, openNew, onDogChange };
|
||||
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ window.Page_map = (() => {
|
|||
|
||||
// z: zIndexOffset — höher = weiter oben bei Überlappung
|
||||
const TYPEN = {
|
||||
restaurant: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#fork-knife"></use></svg>', label: 'Restaurant', color: '#F97316', z: 10 },
|
||||
restaurant: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#fork-knife"></use></svg>', label: 'Hundefreundl. Café/Restaurant', color: '#F97316', z: 10 },
|
||||
freilauf: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>', label: 'Freilauf', color: '#22C55E', z: 20 },
|
||||
shop: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#shopping-cart"></use></svg>', label: 'Shop', color: '#3B82F6', z: 15 },
|
||||
kotbeutel: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#bag"></use></svg>', label: 'Kotbeutel', color: '#84A98C', z: 5 },
|
||||
|
|
@ -92,6 +92,7 @@ window.Page_map = (() => {
|
|||
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 },
|
||||
hotel: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#bed"></use></svg>', label: 'Hundefreundl. Hotel', color: '#0369a1', z: 20 },
|
||||
};
|
||||
|
||||
// Frontend-Layer → Backend-Typ Mapping
|
||||
|
|
@ -109,6 +110,7 @@ window.Page_map = (() => {
|
|||
parkplatz: 'parkplatz',
|
||||
treffpunkt: 'treffpunkt',
|
||||
community: 'sonstiges',
|
||||
hotel: 'hotel',
|
||||
};
|
||||
|
||||
// Gefahren-Radius-Kreis: prominente rote Fläche
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue