Compare commits
2 commits
8c2bc0c445
...
7b3041fc94
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b3041fc94 | |||
| a356626d39 |
21 changed files with 295 additions and 786 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1131
|
||||
1133
|
||||
|
|
@ -15,7 +15,9 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
|||
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".pdf"}
|
||||
|
||||
# Erlaubte Typen
|
||||
TYPEN = {"impfung", "entwurmung", "tierarzt", "medikament", "gewicht", "allergie", "dokument", "laeufigkeit"}
|
||||
# Routine-/Pflege-Typen (wiederkehrend mit intervall_tage): parasit, krallen, fellpflege
|
||||
TYPEN = {"impfung", "entwurmung", "tierarzt", "medikament", "gewicht", "allergie", "dokument",
|
||||
"laeufigkeit", "parasit", "krallen", "fellpflege"}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -164,15 +166,15 @@ async def create_health(dog_id: int, data: HealthCreate,
|
|||
(dog_id, typ, bezeichnung, datum, naechstes, notiz,
|
||||
wert, einheit, charge_nr, tierarzt_name, kosten, diagnose,
|
||||
dosierung, haeufigkeit, aktiv, bis_datum,
|
||||
schweregrad, reaktion, erinnerung, tierarzt_id,
|
||||
schweregrad, reaktion, erinnerung, intervall_tage, tierarzt_id,
|
||||
deckdatum, wurftermin)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(dog_id, data.typ, data.bezeichnung, data.datum, data.naechstes,
|
||||
data.notiz, data.wert, data.einheit, data.charge_nr,
|
||||
data.tierarzt_name, data.kosten, data.diagnose, data.dosierung,
|
||||
data.haeufigkeit, data.aktiv, data.bis_datum,
|
||||
data.schweregrad, data.reaktion, data.erinnerung, data.tierarzt_id,
|
||||
data.deckdatum, data.wurftermin)
|
||||
data.schweregrad, data.reaktion, data.erinnerung, data.intervall_tage,
|
||||
data.tierarzt_id, data.deckdatum, data.wurftermin)
|
||||
)
|
||||
row = conn.execute(
|
||||
"SELECT * FROM health WHERE dog_id=? ORDER BY id DESC LIMIT 1",
|
||||
|
|
@ -212,6 +214,34 @@ async def update_health(dog_id: int, entry_id: int, data: HealthUpdate,
|
|||
return _entry_with_media(row, media_map)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /api/dogs/{dog_id}/health/{id}/erledigt
|
||||
# Markiert eine wiederkehrende Routine als heute erledigt und schreibt
|
||||
# bei gesetztem intervall_tage das nächste Fälligkeitsdatum automatisch fort.
|
||||
# ------------------------------------------------------------------
|
||||
@router.post("/{dog_id}/health/{entry_id}/erledigt")
|
||||
async def complete_health(dog_id: int, entry_id: int, user=Depends(get_current_user)):
|
||||
from datetime import timedelta
|
||||
today = date.today()
|
||||
with db() as conn:
|
||||
_check_dog_owner(conn, dog_id, user["id"])
|
||||
entry = conn.execute(
|
||||
"SELECT * FROM health WHERE id=? AND dog_id=?", (entry_id, dog_id)
|
||||
).fetchone()
|
||||
if not entry:
|
||||
raise HTTPException(404, "Eintrag nicht gefunden.")
|
||||
|
||||
intervall = entry["intervall_tage"]
|
||||
naechstes = (today + timedelta(days=intervall)).isoformat() if intervall else None
|
||||
conn.execute(
|
||||
"UPDATE health SET datum=?, naechstes=? WHERE id=?",
|
||||
(today.isoformat(), naechstes, entry_id),
|
||||
)
|
||||
row = conn.execute("SELECT * FROM health WHERE id=?", (entry_id,)).fetchone()
|
||||
media_map = _fetch_media_items(conn, [entry_id])
|
||||
return _entry_with_media(row, media_map)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DELETE /api/dogs/{dog_id}/health/{id}
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -500,6 +530,9 @@ _TERMIN_TYPEN = {
|
|||
'tierarzt': {'label': 'Tierarztbesuch','beim_tierarzt': True, 'icon': 'first-aid'},
|
||||
'medikament': {'label': 'Medikament', 'beim_tierarzt': False, 'icon': 'pill'},
|
||||
'laeufigkeit': {'label': 'Läufigkeit', 'beim_tierarzt': False, 'icon': 'calendar'},
|
||||
'parasit': {'label': 'Zecken-/Flohschutz', 'beim_tierarzt': False, 'icon': 'bug-beetle'},
|
||||
'krallen': {'label': 'Krallen schneiden', 'beim_tierarzt': False, 'icon': 'scissors'},
|
||||
'fellpflege': {'label': 'Fellpflege', 'beim_tierarzt': False, 'icon': 'wind'},
|
||||
}
|
||||
|
||||
@router.get("/{dog_id}/health/terminvorschlaege")
|
||||
|
|
|
|||
|
|
@ -268,14 +268,20 @@ async def update_application(
|
|||
"UPDATE users SET is_social_media=1 WHERE id=?",
|
||||
(row["user_id"],)
|
||||
)
|
||||
founder_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM users WHERE is_founder=1"
|
||||
).fetchone()[0]
|
||||
if founder_count < 100:
|
||||
conn.execute(
|
||||
"UPDATE users SET is_founder=1 WHERE id=? AND is_founder=0",
|
||||
(row["user_id"],)
|
||||
)
|
||||
# Atomare Gründer-Vergabe inkl. founder_number — Race-frei via Sub-Query
|
||||
# (konsistent mit dogs.py / partner.py).
|
||||
conn.execute(
|
||||
"""UPDATE users
|
||||
SET is_founder = 1,
|
||||
founder_number = (
|
||||
SELECT IFNULL(MAX(founder_number), 0) + 1
|
||||
FROM users WHERE is_founder = 1
|
||||
)
|
||||
WHERE id = ?
|
||||
AND (is_founder IS NULL OR is_founder = 0)
|
||||
AND (SELECT COUNT(*) FROM users WHERE is_founder = 1) < 100""",
|
||||
(row["user_id"],)
|
||||
)
|
||||
|
||||
# Status-Mail an Bewerber
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -638,7 +638,8 @@ async def _job_health_reminders():
|
|||
FROM health h
|
||||
JOIN dogs d ON d.id = h.dog_id
|
||||
WHERE h.naechstes IN (?, ?, ?, ?)
|
||||
AND h.typ IN ('impfung', 'entwurmung', 'medikament')
|
||||
AND h.typ IN ('impfung', 'entwurmung', 'medikament',
|
||||
'parasit', 'krallen', 'fellpflege')
|
||||
AND (h.erinnerung IS NULL OR h.erinnerung = 1)
|
||||
""", (str(today), str(in7), str(in3), str(yesterday))).fetchall()
|
||||
|
||||
|
|
|
|||
|
|
@ -86,14 +86,14 @@
|
|||
<title>Ban Yaro</title>
|
||||
|
||||
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
||||
<script src="/js/boot-early.js?v=1131"></script>
|
||||
<script src="/js/boot-early.js?v=1133"></script>
|
||||
|
||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1131">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1131">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1131">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1131">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1131">
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1133">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1133">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1133">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1133">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1133">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
@ -617,11 +617,11 @@
|
|||
<div id="modal-container"></div>
|
||||
|
||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||
<script src="/js/api.js?v=1131"></script>
|
||||
<script src="/js/ui.js?v=1131"></script>
|
||||
<script src="/js/app.js?v=1131"></script>
|
||||
<script src="/js/worlds.js?v=1131"></script>
|
||||
<script src="/js/offline-indicator.js?v=1131"></script>
|
||||
<script src="/js/api.js?v=1133"></script>
|
||||
<script src="/js/ui.js?v=1133"></script>
|
||||
<script src="/js/app.js?v=1133"></script>
|
||||
<script src="/js/worlds.js?v=1133"></script>
|
||||
<script src="/js/offline-indicator.js?v=1133"></script>
|
||||
|
||||
<!-- Feature-Seiten werden lazy geladen -->
|
||||
|
||||
|
|
@ -631,7 +631,7 @@
|
|||
|
||||
|
||||
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
|
||||
<script src="/js/boot.js?v=1131"></script>
|
||||
<script src="/js/boot.js?v=1133"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ const API = (() => {
|
|||
},
|
||||
create(dogId, data) { return post(`/dogs/${dogId}/health`, data); },
|
||||
update(dogId, id, d) { return patch(`/dogs/${dogId}/health/${id}`, d); },
|
||||
complete(dogId, id) { return post(`/dogs/${dogId}/health/${id}/erledigt`); },
|
||||
delete(dogId, id) { return del(`/dogs/${dogId}/health/${id}`); },
|
||||
uploadDokument(dogId, id, formData) {
|
||||
return upload(`/dogs/${dogId}/health/${id}/dokument`, formData);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '1131'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '1133'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
|
||||
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
|
||||
window.APP_VERSION = APP_VERSION;
|
||||
|
|
|
|||
|
|
@ -782,7 +782,7 @@ window.Page_diary = (() => {
|
|||
const id = parseInt(btn.dataset.entryId);
|
||||
const label = btn.dataset.label || '';
|
||||
const location = btn.dataset.location || null;
|
||||
_openNoteModal('diary', id, label, location || null);
|
||||
UI.noteModal('diary', id, label, location || null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1190,7 +1190,7 @@ window.Page_diary = (() => {
|
|||
view.querySelector('#diary-dv-note')?.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const label = entry.titel || entry.datum || String(entry.id);
|
||||
_openNoteModal('diary', entry.id, label, entry.location_name || null);
|
||||
UI.noteModal('diary', entry.id, label, entry.location_name || null);
|
||||
});
|
||||
|
||||
// Bearbeiten
|
||||
|
|
@ -1953,83 +1953,6 @@ window.Page_diary = (() => {
|
|||
// ----------------------------------------------------------
|
||||
// NOTIZ-MODAL (custom DOM, kein UI.modal um Konflikte zu vermeiden)
|
||||
// ----------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
document.getElementById('by-note-modal')?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'by-note-modal';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:2000;background:rgba(0,0,0,.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div style="background:var(--c-surface);border-radius:var(--radius-xl) var(--radius-xl) 0 0;
|
||||
width:100%;max-width:640px;max-height:90vh;display:flex;flex-direction:column;
|
||||
padding-bottom:env(safe-area-inset-bottom,0px)">
|
||||
<div style="padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--c-border);
|
||||
display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
|
||||
<div>
|
||||
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${UI.escape(parentLabel)}</div>
|
||||
</div>
|
||||
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div style="padding:var(--space-4) var(--space-5);flex:1;overflow-y:auto">
|
||||
<form id="by-note-form">
|
||||
<textarea id="by-note-text" class="form-control" rows="5"
|
||||
placeholder="Notiz eingeben…"
|
||||
style="width:100%;resize:vertical"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
<div style="padding:var(--space-3) var(--space-5);border-top:1px solid var(--c-border);
|
||||
display:flex;gap:var(--space-2);flex-shrink:0">
|
||||
<button type="button" id="by-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button type="submit" form="by-note-form" id="by-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const textarea = document.getElementById('by-note-text');
|
||||
const saveBtn = document.getElementById('by-note-save');
|
||||
const cancelBtn = document.getElementById('by-note-cancel');
|
||||
const closeBtn = document.getElementById('by-note-close');
|
||||
|
||||
let existingNoteId = null;
|
||||
|
||||
try {
|
||||
const existing = await API.notes.get(parentType, parentId);
|
||||
if (existing?.id) {
|
||||
existingNoteId = existing.id;
|
||||
textarea.value = existing.text || '';
|
||||
}
|
||||
} catch (_) { /* keine Notiz vorhanden — ok */ }
|
||||
|
||||
setTimeout(() => textarea.focus(), 100);
|
||||
|
||||
const _close = () => overlay.remove();
|
||||
closeBtn.addEventListener('click', _close);
|
||||
cancelBtn.addEventListener('click', _close);
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) _close(); });
|
||||
|
||||
document.getElementById('by-note-form').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const text = textarea.value.trim();
|
||||
UI.setLoading(saveBtn, true);
|
||||
try {
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName, client_time: API.clientNow() };
|
||||
if (existingNoteId) {
|
||||
await API.notes.update(existingNoteId, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, parentId, payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
_close();
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Speichern.');
|
||||
UI.setLoading(saveBtn, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PUBLIC
|
||||
|
|
|
|||
|
|
@ -461,7 +461,7 @@ window.Page_erste_hilfe = (() => {
|
|||
const titel = btn.dataset.titel;
|
||||
const kat = KATEGORIEN.find(k => k.id === katId);
|
||||
const label = kat ? `${kat.label} — ${titel}` : titel;
|
||||
_openNoteModal('erste_hilfe', katId, label, null);
|
||||
UI.noteModal('erste_hilfe', katId, label, null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -469,85 +469,6 @@ window.Page_erste_hilfe = (() => {
|
|||
// ----------------------------------------------------------------
|
||||
// NOTIZ-MODAL (custom DOM, kein UI.modal um Konflikte zu vermeiden)
|
||||
// ----------------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
document.getElementById('by-note-modal')?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'by-note-modal';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:2000;background:rgba(0,0,0,.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
|
||||
const _esc = s => s ? String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"') : '';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div style="background:var(--c-surface);border-radius:var(--radius-xl) var(--radius-xl) 0 0;
|
||||
width:100%;max-width:640px;max-height:90vh;display:flex;flex-direction:column;
|
||||
padding-bottom:env(safe-area-inset-bottom,0px)">
|
||||
<div style="padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--c-border);
|
||||
display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
|
||||
<div>
|
||||
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${UI.escape(parentLabel)}</div>
|
||||
</div>
|
||||
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div style="padding:var(--space-4) var(--space-5);flex:1;overflow-y:auto">
|
||||
<form id="by-note-form">
|
||||
<textarea id="by-note-text" class="form-control" rows="5"
|
||||
placeholder="Notiz eingeben…"
|
||||
style="width:100%;resize:vertical"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
<div style="padding:var(--space-3) var(--space-5);border-top:1px solid var(--c-border);
|
||||
display:flex;gap:var(--space-2);flex-shrink:0">
|
||||
<button type="button" id="by-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button type="submit" form="by-note-form" id="by-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const textarea = document.getElementById('by-note-text');
|
||||
const saveBtn = document.getElementById('by-note-save');
|
||||
const cancelBtn = document.getElementById('by-note-cancel');
|
||||
const closeBtn = document.getElementById('by-note-close');
|
||||
|
||||
let existingNoteId = null;
|
||||
|
||||
try {
|
||||
const existing = await API.notes.get(parentType, parentId);
|
||||
if (existing?.id) {
|
||||
existingNoteId = existing.id;
|
||||
textarea.value = existing.text || '';
|
||||
}
|
||||
} catch (_) { /* keine Notiz vorhanden — ok */ }
|
||||
|
||||
setTimeout(() => textarea.focus(), 100);
|
||||
|
||||
const _close = () => overlay.remove();
|
||||
closeBtn.addEventListener('click', _close);
|
||||
cancelBtn.addEventListener('click', _close);
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) _close(); });
|
||||
|
||||
document.getElementById('by-note-form').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const text = textarea.value.trim();
|
||||
UI.setLoading(saveBtn, true);
|
||||
try {
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName };
|
||||
if (existingNoteId) {
|
||||
await API.notes.update(existingNoteId, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, parentId, payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
_close();
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Speichern.');
|
||||
UI.setLoading(saveBtn, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// PUBLIC
|
||||
|
|
|
|||
|
|
@ -643,7 +643,7 @@ window.Page_events = (() => {
|
|||
const noteBtn = e.target.closest('.ev-note-btn');
|
||||
if (noteBtn) {
|
||||
e.stopPropagation();
|
||||
_openNoteModal(
|
||||
UI.noteModal(
|
||||
'event',
|
||||
parseInt(noteBtn.dataset.evNoteId),
|
||||
noteBtn.dataset.evNoteLabel,
|
||||
|
|
@ -660,55 +660,6 @@ window.Page_events = (() => {
|
|||
// ----------------------------------------------------------
|
||||
// Notiz-Modal (Custom DOM — kein UI.modal um Konflikte zu vermeiden)
|
||||
// ----------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
let existingNote = null;
|
||||
try { existingNote = await API.notes.get(parentType, String(parentId)); } catch {}
|
||||
|
||||
const ovl = document.createElement('div');
|
||||
ovl.style.cssText = 'position:fixed;inset:0;z-index:1200;background:rgba(0,0,0,0.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
ovl.innerHTML = `
|
||||
<div style="width:100%;max-width:600px;background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
|
||||
padding:var(--space-4);box-sizing:border-box;max-height:80vh;display:flex;flex-direction:column">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
|
||||
<svg class="ph-icon" aria-hidden="true" class="text-primary"><use href="/icons/phosphor.svg#note-pencil"></use></svg>
|
||||
<span style="font-weight:600;flex:1"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz — ${UI.escape(parentLabel)}</span>
|
||||
<button id="ev-note-close" style="background:none;border:none;cursor:pointer;color:var(--c-text-muted);padding:4px">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<textarea id="ev-note-text" rows="5"
|
||||
style="width:100%;box-sizing:border-box;padding:var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
font-size:var(--text-sm);font-family:inherit;
|
||||
background:var(--c-bg);color:var(--c-text);resize:vertical;flex:1"
|
||||
placeholder="Deine Notiz zu diesem Event…">${UI.escape(existingNote?.text || '')}</textarea>
|
||||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3)">
|
||||
<button id="ev-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button id="ev-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(ovl);
|
||||
|
||||
const close = () => ovl.remove();
|
||||
ovl.querySelector('#ev-note-close')?.addEventListener('click', close);
|
||||
ovl.querySelector('#ev-note-cancel')?.addEventListener('click', close);
|
||||
ovl.addEventListener('click', e => { if (e.target === ovl) close(); });
|
||||
|
||||
ovl.querySelector('#ev-note-save')?.addEventListener('click', async () => {
|
||||
const text = ovl.querySelector('#ev-note-text')?.value?.trim() || '';
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName || null };
|
||||
try {
|
||||
if (existingNote?.id) {
|
||||
await API.notes.update(existingNote.id, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, String(parentId), payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
close();
|
||||
} catch (err) { UI.toast.error(err.message || 'Fehler beim Speichern.'); }
|
||||
});
|
||||
}
|
||||
|
||||
return { init, refresh, openNew, _openDetail: _showDetail };
|
||||
|
||||
|
|
|
|||
|
|
@ -442,7 +442,7 @@ window.Page_friends = (() => {
|
|||
e.stopPropagation();
|
||||
const id = parseInt(btn.dataset.frNoteId);
|
||||
const name = btn.dataset.frNoteName || '';
|
||||
_openNoteModal('friends', id, name, null);
|
||||
UI.noteModal('friends', id, name, null);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -866,83 +866,6 @@ window.Page_friends = (() => {
|
|||
// ----------------------------------------------------------
|
||||
// NOTIZ-MODAL
|
||||
// ----------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
document.getElementById('by-note-modal')?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'by-note-modal';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:2000;background:rgba(0,0,0,.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div style="background:var(--c-surface);border-radius:var(--radius-xl) var(--radius-xl) 0 0;
|
||||
width:100%;max-width:640px;max-height:90vh;display:flex;flex-direction:column;
|
||||
padding-bottom:env(safe-area-inset-bottom,0px)">
|
||||
<div style="padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--c-border);
|
||||
display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
|
||||
<div>
|
||||
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${UI.escape(parentLabel)}</div>
|
||||
</div>
|
||||
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div style="padding:var(--space-4) var(--space-5);flex:1;overflow-y:auto">
|
||||
<form id="by-note-form">
|
||||
<textarea id="by-note-text" class="form-control" rows="5"
|
||||
placeholder="Notiz eingeben…"
|
||||
style="width:100%;resize:vertical"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
<div style="padding:var(--space-3) var(--space-5);border-top:1px solid var(--c-border);
|
||||
display:flex;gap:var(--space-2);flex-shrink:0">
|
||||
<button type="button" id="by-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button type="submit" form="by-note-form" id="by-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const textarea = document.getElementById('by-note-text');
|
||||
const saveBtn = document.getElementById('by-note-save');
|
||||
const cancelBtn = document.getElementById('by-note-cancel');
|
||||
const closeBtn = document.getElementById('by-note-close');
|
||||
|
||||
let existingNoteId = null;
|
||||
|
||||
try {
|
||||
const existing = await API.notes.get(parentType, String(parentId));
|
||||
if (existing?.id) {
|
||||
existingNoteId = existing.id;
|
||||
textarea.value = existing.text || '';
|
||||
}
|
||||
} catch (_) { /* keine Notiz vorhanden — ok */ }
|
||||
|
||||
setTimeout(() => textarea.focus(), 100);
|
||||
|
||||
const _close = () => overlay.remove();
|
||||
closeBtn.addEventListener('click', _close);
|
||||
cancelBtn.addEventListener('click', _close);
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) _close(); });
|
||||
|
||||
document.getElementById('by-note-form').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const text = textarea.value.trim();
|
||||
UI.setLoading(saveBtn, true);
|
||||
try {
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName };
|
||||
if (existingNoteId) {
|
||||
await API.notes.update(existingNoteId, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, String(parentId), payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
_close();
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Speichern.');
|
||||
UI.setLoading(saveBtn, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
return { init, refresh, onDogChange, _accept, _decline, _cancel, _removeFriend, _openChat };
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ window.Page_health = (() => {
|
|||
{ key: 'tierarzt', label: 'Besuche', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' },
|
||||
{ key: 'gewicht', label: 'Gewicht', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#scales"></use></svg>' },
|
||||
{ key: 'medikament', label: 'Medikamente', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' },
|
||||
{ key: 'pflege', label: 'Pflege', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paw-print"></use></svg>' },
|
||||
{ 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>' },
|
||||
|
|
@ -27,6 +28,14 @@ window.Page_health = (() => {
|
|||
];
|
||||
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>' };
|
||||
|
||||
// Pflege-Routinen — wiederkehrende Pflege-Aufgaben, gebündelt im 'pflege'-Tab
|
||||
const PFLEGE_TYPEN = ['parasit', 'krallen', 'fellpflege'];
|
||||
const PFLEGE_META = {
|
||||
parasit: { label: 'Zecken-/Flohschutz', icon: 'bug-beetle', placeholder: 'z.B. Frontline, Seresto-Halsband' },
|
||||
krallen: { label: 'Krallen schneiden', icon: 'scissors', placeholder: 'z.B. Krallen kürzen' },
|
||||
fellpflege: { label: 'Fellpflege', icon: 'wind', placeholder: 'z.B. Bürsten, Trimmen, Baden' },
|
||||
};
|
||||
|
||||
function _getTabs() {
|
||||
const tabs = [...BASE_TABS];
|
||||
if (_appState?.activeDog?.geschlecht === 'w') tabs.splice(2, 0, LAEUFIGKEIT_TAB);
|
||||
|
|
@ -290,6 +299,8 @@ window.Page_health = (() => {
|
|||
_data = {};
|
||||
_getTabs().forEach(t => { _data[t.key] = []; });
|
||||
_data['laeufigkeit'] = _data['laeufigkeit'] || [];
|
||||
// Pflege-Routinen: eigene Listen je Typ (Tab 'pflege' bündelt sie beim Rendern)
|
||||
PFLEGE_TYPEN.forEach(t => { _data[t] = []; });
|
||||
all.forEach(e => {
|
||||
if (_data[e.typ] !== undefined) _data[e.typ].push(e);
|
||||
});
|
||||
|
|
@ -333,6 +344,7 @@ window.Page_health = (() => {
|
|||
case 'gewicht': content.innerHTML = _renderGewicht(entries); break;
|
||||
case 'laeufigkeit': content.innerHTML = _renderLaeufigkeit(entries); break;
|
||||
case 'medikament': content.innerHTML = _renderMedikamente(entries); break;
|
||||
case 'pflege': content.innerHTML = _renderPflege(); break;
|
||||
case 'allergie': content.innerHTML = _renderAllergien(entries); break;
|
||||
case 'dokument': content.innerHTML = _renderDokumente(entries); break;
|
||||
case 'praxen': content.innerHTML = _renderPraxen(); break;
|
||||
|
|
@ -410,6 +422,65 @@ window.Page_health = (() => {
|
|||
return { color: 'green', label: 'Aktuell', icon: '🟢' };
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PFLEGE-ROUTINEN (Zecken-/Flohschutz, Krallen, Fellpflege)
|
||||
// ----------------------------------------------------------
|
||||
function _intervallLabel(tage) {
|
||||
if (!tage) return '';
|
||||
const m = { 30: 'monatlich', 60: 'alle 2 Monate', 90: 'vierteljährlich', 180: 'halbjährlich', 365: 'jährlich' };
|
||||
return m[tage] || `alle ${tage} Tage`;
|
||||
}
|
||||
|
||||
function _renderPflege() {
|
||||
const addButtons = `
|
||||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-bottom:var(--space-3)">
|
||||
${PFLEGE_TYPEN.map(t => `
|
||||
<button class="btn btn-secondary btn-sm" data-action="add-routine" data-typ="${t}">
|
||||
${UI.icon(PFLEGE_META[t].icon)} ${PFLEGE_META[t].label}
|
||||
</button>`).join('')}
|
||||
</div>`;
|
||||
|
||||
const all = PFLEGE_TYPEN.flatMap(t => (_data[t] || []).map(e => ({ ...e, _typ: t })));
|
||||
|
||||
if (!all.length) return addButtons + _emptyState(
|
||||
'paw-print',
|
||||
'Noch keine Pflege-Routinen',
|
||||
'Lege wiederkehrende Routinen wie Zecken-/Flohschutz, Krallenschneiden oder Fellpflege an — wir erinnern dich rechtzeitig.'
|
||||
);
|
||||
|
||||
// Fällige zuerst (nach naechstes), Einträge ohne Folgedatum ans Ende
|
||||
all.sort((a, b) => {
|
||||
if (!a.naechstes) return 1;
|
||||
if (!b.naechstes) return -1;
|
||||
return a.naechstes.localeCompare(b.naechstes);
|
||||
});
|
||||
|
||||
const items = all.map(e => {
|
||||
const meta = PFLEGE_META[e._typ];
|
||||
const ampel = e.naechstes ? _impfAmpel(e.naechstes) : null;
|
||||
const interv = _intervallLabel(e.intervall_tage);
|
||||
return `
|
||||
<div class="list-item-card list-item-card--clickable health-card" data-id="${e.id}" data-action="open-entry">
|
||||
${ampel ? `<div class="health-card-ampel ampel-${ampel.color}" title="${ampel.label}"></div>` : ''}
|
||||
<div class="list-item-body">
|
||||
<div class="list-item-title">${UI.icon(meta.icon)} ${UI.escape(e.bezeichnung || meta.label)}</div>
|
||||
<div class="list-item-meta-row">
|
||||
${meta.label}${e.datum ? ` · zuletzt ${UI.time.format(e.datum + 'T00:00:00')}` : ''}${interv ? ` · ${interv}` : ''}
|
||||
</div>
|
||||
${e.naechstes ? `<div class="health-card-next ampel-text-${ampel.color}">
|
||||
Nächste: ${UI.time.format(e.naechstes + 'T00:00:00')} ${ampel.icon}
|
||||
</div>` : ''}
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" data-action="routine-erledigt" data-id="${e.id}"
|
||||
style="flex-shrink:0;white-space:nowrap">
|
||||
${UI.icon('check')} Erledigt
|
||||
</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
return addButtons + `<div class="health-list">${items}</div>`;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// TIERARZTBESUCHE
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -883,21 +954,51 @@ window.Page_health = (() => {
|
|||
// ----------------------------------------------------------
|
||||
// EVENTS BINDEN
|
||||
// ----------------------------------------------------------
|
||||
// Sucht einen Eintrag in der/den Liste(n) des aktiven Tabs.
|
||||
// Im Pflege-Tab sind die Einträge auf mehrere Typ-Listen verteilt.
|
||||
function _entriesForActiveTab() {
|
||||
if (_activeTab === 'pflege') return PFLEGE_TYPEN.flatMap(t => _data[t] || []);
|
||||
return _data[_activeTab] || [];
|
||||
}
|
||||
|
||||
function _bindTabEvents(content) {
|
||||
content.querySelectorAll('[data-action="add-entry"]').forEach(btn => {
|
||||
btn.addEventListener('click', () => _showForm(null, _activeTab));
|
||||
});
|
||||
// Pflege: pro-Typ-Button "+ Routine" → Formular mit festem Typ
|
||||
content.querySelectorAll('[data-action="add-routine"]').forEach(btn => {
|
||||
btn.addEventListener('click', () => _showForm(null, btn.dataset.typ));
|
||||
});
|
||||
// Pflege: Routine als erledigt markieren → Backend schreibt naechstes fort
|
||||
content.querySelectorAll('[data-action="routine-erledigt"]').forEach(btn => {
|
||||
btn.addEventListener('click', async e => {
|
||||
e.stopPropagation();
|
||||
const id = parseInt(btn.dataset.id);
|
||||
await UI.asyncButton(btn, async () => {
|
||||
const saved = await API.health.complete(_appState.activeDog.id, id);
|
||||
const list = _data[saved.typ];
|
||||
if (list) {
|
||||
const idx = list.findIndex(x => x.id === id);
|
||||
if (idx !== -1) list[idx] = saved;
|
||||
}
|
||||
_renderTab();
|
||||
_renderErinnerungen();
|
||||
UI.toast.success('Als erledigt eingetragen.');
|
||||
});
|
||||
});
|
||||
});
|
||||
content.querySelectorAll('[data-action="open-entry"]').forEach(card => {
|
||||
const id = parseInt(card.dataset.id);
|
||||
const entry = (_data[_activeTab] || []).find(e => e.id === id);
|
||||
if (entry) card.addEventListener('click', () => _openDetail(entry));
|
||||
const entry = _entriesForActiveTab().find(e => e.id === id);
|
||||
if (entry) card.addEventListener('click', () =>
|
||||
_activeTab === 'pflege' ? _showForm(entry, entry.typ) : _openDetail(entry));
|
||||
});
|
||||
content.querySelectorAll('[data-action="open-note"]').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const id = parseInt(btn.dataset.entryId);
|
||||
const label = btn.dataset.label || '';
|
||||
_openNoteModal('health', id, label, null);
|
||||
UI.noteModal('health', id, label, null);
|
||||
});
|
||||
});
|
||||
// Praxis öffnen → Detail-Modal mit Bewertungen
|
||||
|
|
@ -980,8 +1081,19 @@ window.Page_health = (() => {
|
|||
// ----------------------------------------------------------
|
||||
// DETAIL-ANSICHT
|
||||
// ----------------------------------------------------------
|
||||
// Tab-Info (Icon + Label) für einen Typ — kennt auch die Pflege-Routine-Typen,
|
||||
// die keinen eigenen Tab haben (sie liegen im gebündelten 'pflege'-Tab).
|
||||
function _typInfo(typ) {
|
||||
const meta = PFLEGE_META[typ];
|
||||
if (meta) return {
|
||||
icon: `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${meta.icon}"></use></svg>`,
|
||||
label: meta.label,
|
||||
};
|
||||
return _getTabs().find(t => t.key === typ) || BASE_TABS[0];
|
||||
}
|
||||
|
||||
function _openDetail(entry) {
|
||||
const tabInfo = _getTabs().find(t => t.key === entry.typ) || BASE_TABS[0];
|
||||
const tabInfo = _typInfo(entry.typ);
|
||||
const fields = _detailFields(entry);
|
||||
|
||||
// Media-Items zusammenstellen (neue + legacy)
|
||||
|
|
@ -1151,7 +1263,7 @@ window.Page_health = (() => {
|
|||
</div>
|
||||
`;
|
||||
|
||||
const tabInfo = _getTabs().find(tab => tab.key === t) || BASE_TABS[0];
|
||||
const tabInfo = _typInfo(t);
|
||||
UI.modal.open({ title: `${tabInfo.icon} ${isEdit ? 'Bearbeiten' : tabInfo.label}`, body, footer });
|
||||
|
||||
const form = document.getElementById('health-form');
|
||||
|
|
@ -1294,6 +1406,9 @@ window.Page_health = (() => {
|
|||
allergie: 'z.B. Hühnchen, Gras, Hausstaub',
|
||||
dokument: 'z.B. Impfpass, Blutbild',
|
||||
laeufigkeit: 'Läufigkeit',
|
||||
parasit: 'z.B. Frontline, Seresto-Halsband',
|
||||
krallen: 'z.B. Krallen kürzen',
|
||||
fellpflege: 'z.B. Bürsten, Trimmen, Baden',
|
||||
};
|
||||
return ph[typ] || '';
|
||||
}
|
||||
|
|
@ -1363,6 +1478,17 @@ window.Page_health = (() => {
|
|||
</div>
|
||||
${_praxisSelectField(entry)}
|
||||
`;
|
||||
case 'parasit':
|
||||
case 'krallen':
|
||||
case 'fellpflege': return `
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:var(--space-3)">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nächste Fälligkeit</label>
|
||||
<input class="form-control" type="date" name="naechstes" value="${entry?.naechstes || ''}">
|
||||
</div>
|
||||
${_intervallField(entry)}
|
||||
</div>
|
||||
`;
|
||||
case 'tierarzt': {
|
||||
const aktivePraxen = _praxen.filter(p => p.aktiv);
|
||||
const praxisField = aktivePraxen.length
|
||||
|
|
@ -2849,85 +2975,6 @@ function _showPoiKorrekturModal(osmId, poiName, currentOh) {
|
|||
// ----------------------------------------------------------
|
||||
// NOTIZ-MODAL (custom DOM, kein UI.modal um Konflikte zu vermeiden)
|
||||
// ----------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
// Vorhandenes Modal entfernen falls noch offen
|
||||
document.getElementById('by-note-modal')?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'by-note-modal';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:2000;background:rgba(0,0,0,.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div style="background:var(--c-surface);border-radius:var(--radius-xl) var(--radius-xl) 0 0;
|
||||
width:100%;max-width:640px;max-height:90vh;display:flex;flex-direction:column;
|
||||
padding-bottom:env(safe-area-inset-bottom,0px)">
|
||||
<div style="padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--c-border);
|
||||
display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
|
||||
<div>
|
||||
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${UI.escape(parentLabel)}</div>
|
||||
</div>
|
||||
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div style="padding:var(--space-4) var(--space-5);flex:1;overflow-y:auto">
|
||||
<form id="by-note-form">
|
||||
<textarea id="by-note-text" class="form-control" rows="5"
|
||||
placeholder="Notiz eingeben…"
|
||||
style="width:100%;resize:vertical"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
<div style="padding:var(--space-3) var(--space-5);border-top:1px solid var(--c-border);
|
||||
display:flex;gap:var(--space-2);flex-shrink:0">
|
||||
<button type="button" id="by-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button type="submit" form="by-note-form" id="by-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const textarea = document.getElementById('by-note-text');
|
||||
const saveBtn = document.getElementById('by-note-save');
|
||||
const cancelBtn = document.getElementById('by-note-cancel');
|
||||
const closeBtn = document.getElementById('by-note-close');
|
||||
|
||||
let existingNoteId = null;
|
||||
|
||||
// Vorhandene Notiz laden
|
||||
try {
|
||||
const existing = await API.notes.get(parentType, parentId);
|
||||
if (existing?.id) {
|
||||
existingNoteId = existing.id;
|
||||
textarea.value = existing.text || '';
|
||||
}
|
||||
} catch (_) { /* keine Notiz vorhanden — ok */ }
|
||||
|
||||
setTimeout(() => textarea.focus(), 100);
|
||||
|
||||
const _close = () => overlay.remove();
|
||||
closeBtn.addEventListener('click', _close);
|
||||
cancelBtn.addEventListener('click', _close);
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) _close(); });
|
||||
|
||||
document.getElementById('by-note-form').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const text = textarea.value.trim();
|
||||
UI.setLoading(saveBtn, true);
|
||||
try {
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName };
|
||||
if (existingNoteId) {
|
||||
await API.notes.update(existingNoteId, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, parentId, payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
_close();
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Speichern.');
|
||||
UI.setLoading(saveBtn, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// KI-TIERARZTFRAGEN
|
||||
|
|
|
|||
|
|
@ -353,7 +353,7 @@ window.Page_lost = (() => {
|
|||
e.stopPropagation();
|
||||
const id = parseInt(btn.dataset.lostNoteId);
|
||||
const name = btn.dataset.lostNoteName || '';
|
||||
_openNoteModal('lost', id, name, null);
|
||||
UI.noteModal('lost', id, name, null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -804,83 +804,6 @@ function _emptyState(icon, title, text, cta = '') {
|
|||
// ----------------------------------------------------------
|
||||
// NOTIZ-MODAL
|
||||
// ----------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
document.getElementById('by-note-modal')?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'by-note-modal';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:2000;background:rgba(0,0,0,.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div style="background:var(--c-surface);border-radius:var(--radius-xl) var(--radius-xl) 0 0;
|
||||
width:100%;max-width:640px;max-height:90vh;display:flex;flex-direction:column;
|
||||
padding-bottom:env(safe-area-inset-bottom,0px)">
|
||||
<div style="padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--c-border);
|
||||
display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
|
||||
<div>
|
||||
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${UI.escape(parentLabel)}</div>
|
||||
</div>
|
||||
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div style="padding:var(--space-4) var(--space-5);flex:1;overflow-y:auto">
|
||||
<form id="by-note-form">
|
||||
<textarea id="by-note-text" class="form-control" rows="5"
|
||||
placeholder="Notiz eingeben…"
|
||||
style="width:100%;resize:vertical"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
<div style="padding:var(--space-3) var(--space-5);border-top:1px solid var(--c-border);
|
||||
display:flex;gap:var(--space-2);flex-shrink:0">
|
||||
<button type="button" id="by-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button type="submit" form="by-note-form" id="by-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const textarea = document.getElementById('by-note-text');
|
||||
const saveBtn = document.getElementById('by-note-save');
|
||||
const cancelBtn = document.getElementById('by-note-cancel');
|
||||
const closeBtn = document.getElementById('by-note-close');
|
||||
|
||||
let existingNoteId = null;
|
||||
|
||||
try {
|
||||
const existing = await API.notes.get(parentType, String(parentId));
|
||||
if (existing?.id) {
|
||||
existingNoteId = existing.id;
|
||||
textarea.value = existing.text || '';
|
||||
}
|
||||
} catch (_) { /* keine Notiz vorhanden — ok */ }
|
||||
|
||||
setTimeout(() => textarea.focus(), 100);
|
||||
|
||||
const _close = () => overlay.remove();
|
||||
closeBtn.addEventListener('click', _close);
|
||||
cancelBtn.addEventListener('click', _close);
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) _close(); });
|
||||
|
||||
document.getElementById('by-note-form').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const text = textarea.value.trim();
|
||||
UI.setLoading(saveBtn, true);
|
||||
try {
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName };
|
||||
if (existingNoteId) {
|
||||
await API.notes.update(existingNoteId, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, String(parentId), payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
_close();
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Speichern.');
|
||||
UI.setLoading(saveBtn, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PUBLIC
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ window.Page_poison = (() => {
|
|||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const id = parseInt(btn.dataset.poisonNoteId);
|
||||
_openNoteModal('poison', id, 'Giftköder-Meldung ' + id, null);
|
||||
UI.noteModal('poison', id, 'Giftköder-Meldung ' + id, null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -650,83 +650,6 @@ window.Page_poison = (() => {
|
|||
// ----------------------------------------------------------
|
||||
// NOTIZ-MODAL
|
||||
// ----------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
document.getElementById('by-note-modal')?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'by-note-modal';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:2000;background:rgba(0,0,0,.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div style="background:var(--c-surface);border-radius:var(--radius-xl) var(--radius-xl) 0 0;
|
||||
width:100%;max-width:640px;max-height:90vh;display:flex;flex-direction:column;
|
||||
padding-bottom:env(safe-area-inset-bottom,0px)">
|
||||
<div style="padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--c-border);
|
||||
display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
|
||||
<div>
|
||||
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${UI.escape(parentLabel)}</div>
|
||||
</div>
|
||||
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div style="padding:var(--space-4) var(--space-5);flex:1;overflow-y:auto">
|
||||
<form id="by-note-form">
|
||||
<textarea id="by-note-text" class="form-control" rows="5"
|
||||
placeholder="Notiz eingeben…"
|
||||
style="width:100%;resize:vertical"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
<div style="padding:var(--space-3) var(--space-5);border-top:1px solid var(--c-border);
|
||||
display:flex;gap:var(--space-2);flex-shrink:0">
|
||||
<button type="button" id="by-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button type="submit" form="by-note-form" id="by-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const textarea = document.getElementById('by-note-text');
|
||||
const saveBtn = document.getElementById('by-note-save');
|
||||
const cancelBtn = document.getElementById('by-note-cancel');
|
||||
const closeBtn = document.getElementById('by-note-close');
|
||||
|
||||
let existingNoteId = null;
|
||||
|
||||
try {
|
||||
const existing = await API.notes.get(parentType, String(parentId));
|
||||
if (existing?.id) {
|
||||
existingNoteId = existing.id;
|
||||
textarea.value = existing.text || '';
|
||||
}
|
||||
} catch (_) { /* keine Notiz vorhanden — ok */ }
|
||||
|
||||
setTimeout(() => textarea.focus(), 100);
|
||||
|
||||
const _close = () => overlay.remove();
|
||||
closeBtn.addEventListener('click', _close);
|
||||
cancelBtn.addEventListener('click', _close);
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) _close(); });
|
||||
|
||||
document.getElementById('by-note-form').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const text = textarea.value.trim();
|
||||
UI.setLoading(saveBtn, true);
|
||||
try {
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName };
|
||||
if (existingNoteId) {
|
||||
await API.notes.update(existingNoteId, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, String(parentId), payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
_close();
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Speichern.');
|
||||
UI.setLoading(saveBtn, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PUBLIC
|
||||
|
|
|
|||
|
|
@ -2397,7 +2397,7 @@ window.Page_routes = (() => {
|
|||
// Notiz-Button
|
||||
document.getElementById('rd-note')?.addEventListener('click', () => {
|
||||
const label = route.name || (route.distanz_km ? route.distanz_km.toFixed(1) + ' km' : 'Route');
|
||||
_openNoteModal('route', route.id, label, null);
|
||||
UI.noteModal('route', route.id, label, null);
|
||||
});
|
||||
|
||||
// Mini-Map
|
||||
|
|
@ -3054,55 +3054,6 @@ window.Page_routes = (() => {
|
|||
// ----------------------------------------------------------
|
||||
// Notiz-Modal (Custom DOM — kein UI.modal um Konflikte zu vermeiden)
|
||||
// ----------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
let existingNote = null;
|
||||
try { existingNote = await API.notes.get(parentType, String(parentId)); } catch {}
|
||||
|
||||
const ovl = document.createElement('div');
|
||||
ovl.style.cssText = 'position:fixed;inset:0;z-index:1200;background:rgba(0,0,0,0.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
ovl.innerHTML = `
|
||||
<div style="width:100%;max-width:600px;background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
|
||||
padding:var(--space-4);box-sizing:border-box;max-height:80vh;display:flex;flex-direction:column">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
|
||||
<svg class="ph-icon" aria-hidden="true" class="text-primary"><use href="/icons/phosphor.svg#note-pencil"></use></svg>
|
||||
<span style="font-weight:600;flex:1"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz — ${UI.escape(parentLabel)}</span>
|
||||
<button id="rk-note-close" style="background:none;border:none;cursor:pointer;color:var(--c-text-muted);padding:4px">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<textarea id="rk-note-text" rows="5"
|
||||
style="width:100%;box-sizing:border-box;padding:var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
font-size:var(--text-sm);font-family:inherit;
|
||||
background:var(--c-bg);color:var(--c-text);resize:vertical;flex:1"
|
||||
placeholder="Deine Notiz zu dieser Route…">${UI.escape(existingNote?.text || '')}</textarea>
|
||||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3)">
|
||||
<button id="rk-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button id="rk-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(ovl);
|
||||
|
||||
const close = () => ovl.remove();
|
||||
ovl.querySelector('#rk-note-close')?.addEventListener('click', close);
|
||||
ovl.querySelector('#rk-note-cancel')?.addEventListener('click', close);
|
||||
ovl.addEventListener('click', e => { if (e.target === ovl) close(); });
|
||||
|
||||
ovl.querySelector('#rk-note-save')?.addEventListener('click', async () => {
|
||||
const text = ovl.querySelector('#rk-note-text')?.value?.trim() || '';
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName || null, client_time: API.clientNow() };
|
||||
try {
|
||||
if (existingNote?.id) {
|
||||
await API.notes.update(existingNote.id, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, String(parentId), payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
close();
|
||||
} catch (err) { UI.toast.error(err.message || 'Fehler beim Speichern.'); }
|
||||
});
|
||||
}
|
||||
|
||||
return { init, refresh, onDogChange };
|
||||
|
||||
|
|
|
|||
|
|
@ -714,7 +714,7 @@ window.Page_sitting = (() => {
|
|||
const noteBtn = e.target.closest('.sit-note-btn');
|
||||
if (noteBtn) {
|
||||
e.stopPropagation();
|
||||
_openNoteModal(
|
||||
UI.noteModal(
|
||||
'sitting',
|
||||
parseInt(noteBtn.dataset.sitNoteId),
|
||||
noteBtn.dataset.sitNoteLabel,
|
||||
|
|
@ -763,55 +763,6 @@ window.Page_sitting = (() => {
|
|||
// ----------------------------------------------------------
|
||||
// Notiz-Modal (Custom DOM — kein UI.modal um Konflikte zu vermeiden)
|
||||
// ----------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
let existingNote = null;
|
||||
try { existingNote = await API.notes.get(parentType, String(parentId)); } catch {}
|
||||
|
||||
const ovl = document.createElement('div');
|
||||
ovl.style.cssText = 'position:fixed;inset:0;z-index:1200;background:rgba(0,0,0,0.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
ovl.innerHTML = `
|
||||
<div style="width:100%;max-width:600px;background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
|
||||
padding:var(--space-4);box-sizing:border-box;max-height:80vh;display:flex;flex-direction:column">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
|
||||
<svg class="ph-icon" aria-hidden="true" class="text-primary"><use href="/icons/phosphor.svg#note-pencil"></use></svg>
|
||||
<span style="font-weight:600;flex:1"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz — ${UI.escape(parentLabel)}</span>
|
||||
<button id="sit-note-close" style="background:none;border:none;cursor:pointer;color:var(--c-text-muted);padding:4px">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<textarea id="sit-note-text" rows="5"
|
||||
style="width:100%;box-sizing:border-box;padding:var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
font-size:var(--text-sm);font-family:inherit;
|
||||
background:var(--c-bg);color:var(--c-text);resize:vertical;flex:1"
|
||||
placeholder="Deine Notiz zu diesem Sitter…">${UI.escape(existingNote?.text || '')}</textarea>
|
||||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3)">
|
||||
<button id="sit-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button id="sit-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(ovl);
|
||||
|
||||
const close = () => ovl.remove();
|
||||
ovl.querySelector('#sit-note-close')?.addEventListener('click', close);
|
||||
ovl.querySelector('#sit-note-cancel')?.addEventListener('click', close);
|
||||
ovl.addEventListener('click', e => { if (e.target === ovl) close(); });
|
||||
|
||||
ovl.querySelector('#sit-note-save')?.addEventListener('click', async () => {
|
||||
const text = ovl.querySelector('#sit-note-text')?.value?.trim() || '';
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName || null };
|
||||
try {
|
||||
if (existingNote?.id) {
|
||||
await API.notes.update(existingNote.id, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, String(parentId), payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
close();
|
||||
} catch (err) { UI.toast.error(err.message || 'Fehler beim Speichern.'); }
|
||||
});
|
||||
}
|
||||
|
||||
return { init, refresh };
|
||||
|
||||
|
|
|
|||
|
|
@ -538,7 +538,7 @@ function _icon(name) {
|
|||
const planLabel = _activePlan === 'welpe' ? 'Welpe 0–6 Monate'
|
||||
: _activePlan === 'junior' ? 'Junior 6–18 Monate'
|
||||
: `Erwachsener Hund – ${_activeAdultTab}`;
|
||||
_openNoteModal('trainingsplan', dogId, planLabel, null);
|
||||
UI.noteModal('trainingsplan', dogId, planLabel, null);
|
||||
});
|
||||
|
||||
// Plan selector
|
||||
|
|
@ -768,84 +768,6 @@ function _icon(name) {
|
|||
// ----------------------------------------------------------
|
||||
// NOTIZ-MODAL
|
||||
// ----------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
// Vorhandenes Modal entfernen falls noch offen
|
||||
document.getElementById('by-note-modal')?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'by-note-modal';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:2000;background:rgba(0,0,0,.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div style="background:var(--c-surface);border-radius:var(--radius-xl) var(--radius-xl) 0 0;
|
||||
width:100%;max-width:640px;max-height:90vh;display:flex;flex-direction:column;
|
||||
padding-bottom:env(safe-area-inset-bottom,0px)">
|
||||
<div style="padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--c-border);
|
||||
display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
|
||||
<div>
|
||||
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${UI.escape(parentLabel)}</div>
|
||||
</div>
|
||||
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div style="padding:var(--space-4) var(--space-5);flex:1;overflow-y:auto">
|
||||
<form id="by-note-form">
|
||||
<textarea id="by-note-text" class="form-control" rows="5"
|
||||
placeholder="Notiz eingeben…"
|
||||
style="width:100%;resize:vertical"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
<div style="padding:var(--space-3) var(--space-5);border-top:1px solid var(--c-border);
|
||||
display:flex;gap:var(--space-2);flex-shrink:0">
|
||||
<button type="button" id="by-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button type="submit" form="by-note-form" id="by-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const textarea = document.getElementById('by-note-text');
|
||||
const saveBtn = document.getElementById('by-note-save');
|
||||
const cancelBtn = document.getElementById('by-note-cancel');
|
||||
const closeBtn = document.getElementById('by-note-close');
|
||||
|
||||
let existingNoteId = null;
|
||||
|
||||
try {
|
||||
const existing = await API.notes.get(parentType, String(parentId));
|
||||
if (existing?.id) {
|
||||
existingNoteId = existing.id;
|
||||
textarea.value = existing.text || '';
|
||||
}
|
||||
} catch (_) { /* keine Notiz vorhanden — ok */ }
|
||||
|
||||
setTimeout(() => textarea.focus(), 100);
|
||||
|
||||
const _close = () => overlay.remove();
|
||||
closeBtn.addEventListener('click', _close);
|
||||
cancelBtn.addEventListener('click', _close);
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) _close(); });
|
||||
|
||||
document.getElementById('by-note-form').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const text = textarea.value.trim();
|
||||
UI.setLoading(saveBtn, true);
|
||||
try {
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName };
|
||||
if (existingNoteId) {
|
||||
await API.notes.update(existingNoteId, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, String(parentId), payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
_close();
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Speichern.');
|
||||
UI.setLoading(saveBtn, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PUBLIC API
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ window.Page_walks = (() => {
|
|||
el.querySelectorAll('.wk-note-btn').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
_openNoteModal(
|
||||
UI.noteModal(
|
||||
'walk',
|
||||
parseInt(btn.dataset.wkNoteId),
|
||||
btn.dataset.wkNoteLabel,
|
||||
|
|
@ -1211,55 +1211,6 @@ window.Page_walks = (() => {
|
|||
// ----------------------------------------------------------
|
||||
// Notiz-Modal (Custom DOM — kein UI.modal um Konflikte zu vermeiden)
|
||||
// ----------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
let existingNote = null;
|
||||
try { existingNote = await API.notes.get(parentType, String(parentId)); } catch {}
|
||||
|
||||
const ovl = document.createElement('div');
|
||||
ovl.style.cssText = 'position:fixed;inset:0;z-index:1200;background:rgba(0,0,0,0.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
ovl.innerHTML = `
|
||||
<div style="width:100%;max-width:600px;background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
|
||||
padding:var(--space-4);box-sizing:border-box;max-height:80vh;display:flex;flex-direction:column">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
|
||||
<svg class="ph-icon" aria-hidden="true" class="text-primary"><use href="/icons/phosphor.svg#note-pencil"></use></svg>
|
||||
<span style="font-weight:600;flex:1"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz — ${UI.escape(parentLabel)}</span>
|
||||
<button id="wk-note-close" style="background:none;border:none;cursor:pointer;color:var(--c-text-muted);padding:4px">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<textarea id="wk-note-text" rows="5"
|
||||
style="width:100%;box-sizing:border-box;padding:var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
font-size:var(--text-sm);font-family:inherit;
|
||||
background:var(--c-bg);color:var(--c-text);resize:vertical;flex:1"
|
||||
placeholder="Deine Notiz zu diesem Gassi-Treffen…">${UI.escape(existingNote?.text || '')}</textarea>
|
||||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3)">
|
||||
<button id="wk-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button id="wk-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(ovl);
|
||||
|
||||
const close = () => ovl.remove();
|
||||
ovl.querySelector('#wk-note-close')?.addEventListener('click', close);
|
||||
ovl.querySelector('#wk-note-cancel')?.addEventListener('click', close);
|
||||
ovl.addEventListener('click', e => { if (e.target === ovl) close(); });
|
||||
|
||||
ovl.querySelector('#wk-note-save')?.addEventListener('click', async () => {
|
||||
const text = ovl.querySelector('#wk-note-text')?.value?.trim() || '';
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName || null };
|
||||
try {
|
||||
if (existingNote?.id) {
|
||||
await API.notes.update(existingNote.id, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, String(parentId), payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
close();
|
||||
} catch (err) { UI.toast.error(err.message || 'Fehler beim Speichern.'); }
|
||||
});
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// FEATURE 1: Foto-Challenge der Woche
|
||||
|
|
|
|||
|
|
@ -1327,9 +1327,91 @@ const UI = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// NOTE-MODAL — Notiz zu einem beliebigen Objekt (parentType/parentId)
|
||||
// erstellen/bearbeiten. Zentral, damit nicht jede Seite eine eigene Kopie hat.
|
||||
// ----------------------------------------------------------
|
||||
async function noteModal(parentType, parentId, parentLabel, locationName) {
|
||||
document.getElementById('by-note-modal')?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'by-note-modal';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:2000;background:rgba(0,0,0,.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div style="background:var(--c-surface);border-radius:var(--radius-xl) var(--radius-xl) 0 0;
|
||||
width:100%;max-width:640px;max-height:90vh;display:flex;flex-direction:column;
|
||||
padding-bottom:env(safe-area-inset-bottom,0px)">
|
||||
<div style="padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--c-border);
|
||||
display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
|
||||
<div>
|
||||
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)">${_svgIcon('note-pencil')} Notiz</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${escape(parentLabel)}</div>
|
||||
</div>
|
||||
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div style="padding:var(--space-4) var(--space-5);flex:1;overflow-y:auto">
|
||||
<form id="by-note-form">
|
||||
<textarea id="by-note-text" class="form-control" rows="5"
|
||||
placeholder="Notiz eingeben…"
|
||||
style="width:100%;resize:vertical"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
<div style="padding:var(--space-3) var(--space-5);border-top:1px solid var(--c-border);
|
||||
display:flex;gap:var(--space-2);flex-shrink:0">
|
||||
<button type="button" id="by-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button type="submit" form="by-note-form" id="by-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const textarea = document.getElementById('by-note-text');
|
||||
const saveBtn = document.getElementById('by-note-save');
|
||||
const cancelBtn = document.getElementById('by-note-cancel');
|
||||
const closeBtn = document.getElementById('by-note-close');
|
||||
|
||||
let existingNoteId = null;
|
||||
try {
|
||||
const existing = await API.notes.get(parentType, parentId);
|
||||
if (existing?.id) {
|
||||
existingNoteId = existing.id;
|
||||
textarea.value = existing.text || '';
|
||||
}
|
||||
} catch (_) { /* keine Notiz vorhanden — ok */ }
|
||||
|
||||
setTimeout(() => textarea.focus(), 100);
|
||||
|
||||
const _close = () => overlay.remove();
|
||||
closeBtn.addEventListener('click', _close);
|
||||
cancelBtn.addEventListener('click', _close);
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) _close(); });
|
||||
|
||||
document.getElementById('by-note-form').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const text = textarea.value.trim();
|
||||
setLoading(saveBtn, true);
|
||||
try {
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName || null, client_time: API.clientNow() };
|
||||
if (existingNoteId) {
|
||||
await API.notes.update(existingNoteId, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, parentId, payload);
|
||||
}
|
||||
toast.success('Notiz gespeichert.');
|
||||
_close();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Fehler beim Speichern.');
|
||||
setLoading(saveBtn, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Öffentliche API
|
||||
return {
|
||||
toast, modal,
|
||||
noteModal,
|
||||
setLoading, asyncButton,
|
||||
formData, setFormError, clearFormErrors,
|
||||
emptyState, errorState, time, text, money,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<script src="/js/landing-init.js?v=1131"></script>
|
||||
<script src="/js/landing-init.js?v=1133"></script>
|
||||
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
|
||||
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store.">
|
||||
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
============================================================ */
|
||||
|
||||
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
|
||||
const VER = '1131';
|
||||
const VER = '1133';
|
||||
const CACHE_VERSION = `by-v${VER}`;
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue