- Volltext-Suche im Tagebuch (LIKE über Titel/Text/Tags, Debounce 350ms)
- Digitaler Heimtierausweis als druckbare HTML-Seite (/ausweis/{dog_id})
Enthält Impfungen, Medikamente, Allergien, Tierärzte, Chip-Nr.
- Hund teilen: Einladungslink-System (dog_shares-Tabelle, /teilen/{token})
Geteilte Hunde erscheinen in der Hundeliste, Tagebuch/Gesundheit lesbar
- Widget-Seite /#widget: zufälliges Tagebuchbild + nächste Erinnerung
Als PWA-Shortcut im Manifest verankert
- SW-Cache by-v144, APP_VER 117
703 lines
28 KiB
JavaScript
703 lines
28 KiB
JavaScript
/* ============================================================
|
|
BAN YARO — Tagebuch (Sprint 1)
|
|
Seiten-Modul: Timeline aller Einträge, Erstellen, Bearbeiten,
|
|
Löschen, Foto-Upload, Meilensteine.
|
|
============================================================ */
|
|
|
|
window.Page_diary = (() => {
|
|
|
|
// ----------------------------------------------------------
|
|
// MODUL-STATE
|
|
// ----------------------------------------------------------
|
|
let _container = null;
|
|
let _appState = null;
|
|
let _entries = [];
|
|
let _offset = 0;
|
|
let _searchQuery = '';
|
|
const LIMIT = 20;
|
|
|
|
const TYPEN = {
|
|
eintrag: { label: 'Eintrag', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>' },
|
|
foto: { label: 'Foto', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg>' },
|
|
meilenstein:{ label: 'Meilenstein',icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trophy"></use></svg>' },
|
|
training: { label: 'Training', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#target"></use></svg>' },
|
|
gesundheit: { label: 'Gesundheit', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#syringe"></use></svg>' },
|
|
ausflug: { label: 'Ausflug', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#car"></use></svg>' },
|
|
};
|
|
|
|
// ----------------------------------------------------------
|
|
// INIT — erster Aufruf, Container leer
|
|
// ----------------------------------------------------------
|
|
async function init(container, appState) {
|
|
_container = container;
|
|
_appState = appState;
|
|
await _render();
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// REFRESH — erneuter Navigations-Aufruf (Tap auf Tab)
|
|
// ----------------------------------------------------------
|
|
async function refresh() {
|
|
if (!_appState.activeDog) return;
|
|
// Mehrere Hunde → Picker zeigen (User kann Hund wählen)
|
|
if (_appState.dogs.length > 1) {
|
|
_renderDogPicker();
|
|
return;
|
|
}
|
|
// Einzelner Hund → Diary direkt neu laden
|
|
_offset = 0;
|
|
_entries = [];
|
|
await _renderDiary();
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// ON DOG CHANGE — vom Header-Switcher ausgelöst
|
|
// ----------------------------------------------------------
|
|
async function onDogChange(dog) {
|
|
_offset = 0;
|
|
_entries = [];
|
|
_searchQuery = '';
|
|
await _renderDiary();
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// OPEN NEW — vom + Button oder Quick-Add
|
|
// ----------------------------------------------------------
|
|
function openNew() {
|
|
_showForm(null);
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// RENDER — Einstieg: Picker bei mehreren Hunden, sonst direkt
|
|
// ----------------------------------------------------------
|
|
async function _render() {
|
|
if (!_appState.activeDog) {
|
|
_container.innerHTML = UI.emptyState({
|
|
icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>',
|
|
title: 'Noch kein Hund angelegt',
|
|
text: 'Erstelle zuerst ein Hundeprofil, um das Tagebuch zu nutzen.',
|
|
action: `<button class="btn btn-primary" id="diary-goto-profile">Profil erstellen</button>`,
|
|
});
|
|
_container.querySelector('#diary-goto-profile')
|
|
?.addEventListener('click', () => App.navigate('dog-profile'));
|
|
return;
|
|
}
|
|
|
|
if (_appState.dogs.length > 1) {
|
|
_renderDogPicker();
|
|
} else {
|
|
await _renderDiary();
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// HUNDE-PICKER — Einstiegsseite bei mehreren Hunden
|
|
// ----------------------------------------------------------
|
|
function _renderDogPicker() {
|
|
const activeDogId = _appState.activeDog?.id;
|
|
|
|
const cards = _appState.dogs.map(dog => {
|
|
const isActive = dog.id === activeDogId;
|
|
const av = dog.foto_url
|
|
? `<img src="${_escape(dog.foto_url)}" alt="${_escape(dog.name)}">`
|
|
: `<span>${UI.icon('dog')}</span>`;
|
|
return `
|
|
<div class="diary-picker-card${isActive ? ' diary-picker-card--active' : ''}"
|
|
data-dog-id="${dog.id}">
|
|
<div class="diary-picker-av">${av}</div>
|
|
<div class="diary-picker-name">${_escape(dog.name)}</div>
|
|
${dog.rasse ? `<div class="diary-picker-rasse">${_escape(dog.rasse)}</div>` : ''}
|
|
</div>`;
|
|
}).join('');
|
|
|
|
_container.innerHTML = `
|
|
<div class="diary-picker-wrap">
|
|
<p class="diary-picker-hint">Wessen Tagebuch?</p>
|
|
<div class="diary-picker-grid">${cards}</div>
|
|
</div>`;
|
|
|
|
_container.querySelectorAll('.diary-picker-card').forEach(el => {
|
|
el.addEventListener('click', async () => {
|
|
const id = parseInt(el.dataset.dogId);
|
|
if (id === _appState.activeDog?.id) {
|
|
// Bereits aktiver Hund → direkt Diary laden
|
|
_offset = 0; _entries = [];
|
|
await _renderDiary();
|
|
} else {
|
|
App.setActiveDog(id);
|
|
// onDogChange() → _renderDiary() via _notifyDogChange()
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// DIARY-ANSICHT — Timeline mit Einträgen
|
|
// ----------------------------------------------------------
|
|
async function _renderDiary() {
|
|
_container.innerHTML = `
|
|
<div class="by-toolbar diary-toolbar">
|
|
<div class="diary-search-wrap" id="diary-search-wrap">
|
|
<svg class="ph-icon diary-search-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>
|
|
<input type="search" class="diary-search-input" id="diary-search-input"
|
|
placeholder="Einträge durchsuchen…" autocomplete="off">
|
|
</div>
|
|
<button class="btn btn-secondary btn-sm" id="diary-import-btn" title="Importieren">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
|
|
</button>
|
|
</div>
|
|
<div id="diary-list"></div>
|
|
<div id="diary-load-more" style="display:none; text-align:center; padding:var(--space-4)">
|
|
<button class="btn btn-secondary" id="diary-btn-more">Weitere laden</button>
|
|
</div>
|
|
`;
|
|
|
|
_container.querySelector('#diary-import-btn')
|
|
?.addEventListener('click', _showImport);
|
|
_container.querySelector('#diary-btn-more')
|
|
?.addEventListener('click', () => _loadMore());
|
|
|
|
// Suche mit Debounce
|
|
let _searchTimer = null;
|
|
_container.querySelector('#diary-search-input')
|
|
?.addEventListener('input', e => {
|
|
clearTimeout(_searchTimer);
|
|
_searchTimer = setTimeout(async () => {
|
|
_offset = 0;
|
|
_entries = [];
|
|
_searchQuery = e.target.value.trim();
|
|
await _load();
|
|
_renderList();
|
|
}, 350);
|
|
});
|
|
|
|
await _load();
|
|
_renderList();
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// DATEN LADEN
|
|
// ----------------------------------------------------------
|
|
async function _load() {
|
|
const dog = _appState.activeDog;
|
|
if (!dog) return;
|
|
try {
|
|
const params = { limit: LIMIT, offset: _offset };
|
|
if (_searchQuery) params.q = _searchQuery;
|
|
const batch = await API.diary.list(dog.id, params);
|
|
_entries = _entries.concat(batch);
|
|
|
|
// "Mehr laden" anzeigen wenn volle Page geladen wurde
|
|
const loadMore = _container.querySelector('#diary-load-more');
|
|
if (loadMore) {
|
|
loadMore.style.display = batch.length === LIMIT ? 'block' : 'none';
|
|
}
|
|
} catch (err) {
|
|
UI.toast.error('Einträge konnten nicht geladen werden.');
|
|
}
|
|
}
|
|
|
|
async function _loadMore() {
|
|
_offset += LIMIT;
|
|
const btn = _container.querySelector('#diary-btn-more');
|
|
UI.setLoading(btn, true);
|
|
await _load();
|
|
_renderList();
|
|
UI.setLoading(btn, false);
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// LISTE RENDERN — Timeline gruppiert nach Monat
|
|
// ----------------------------------------------------------
|
|
function _renderList() {
|
|
const listEl = _container.querySelector('#diary-list');
|
|
if (!listEl) return;
|
|
|
|
if (_entries.length === 0) {
|
|
listEl.innerHTML = UI.emptyState({
|
|
icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>',
|
|
title: 'Noch keine Einträge',
|
|
text: 'Halte besondere Momente mit deinem Hund fest.',
|
|
action: `<button class="btn btn-primary" id="diary-first-entry">Ersten Eintrag erstellen</button>`,
|
|
});
|
|
listEl.querySelector('#diary-first-entry')
|
|
?.addEventListener('click', () => _showForm(null));
|
|
return;
|
|
}
|
|
|
|
// Gruppieren nach Jahr-Monat (Anzeigereihenfolge: chronologisch absteigend)
|
|
const groups = new Map();
|
|
_entries.forEach(e => {
|
|
const key = e.datum ? e.datum.slice(0, 7) : 'unbekannt'; // "2025-04"
|
|
if (!groups.has(key)) groups.set(key, []);
|
|
groups.get(key).push(e);
|
|
});
|
|
|
|
let html = '';
|
|
groups.forEach((items, key) => {
|
|
const monthLabel = key === 'unbekannt' ? 'Datum unbekannt' : _formatMonth(key);
|
|
html += `<div class="diary-month-header">${monthLabel}</div>`;
|
|
html += items.map(e => _entryCard(e)).join('');
|
|
});
|
|
|
|
listEl.innerHTML = html;
|
|
|
|
// Events an Karten binden
|
|
listEl.querySelectorAll('[data-entry-id]').forEach(card => {
|
|
const id = parseInt(card.dataset.entryId);
|
|
card.addEventListener('click', () => _openDetail(id));
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// ENTRY CARD
|
|
// ----------------------------------------------------------
|
|
function _entryCard(e) {
|
|
const typ = TYPEN[e.typ] || TYPEN.eintrag;
|
|
const isMile = e.is_milestone || e.typ === 'meilenstein';
|
|
const dateStr = e.datum ? UI.time.format(e.datum + 'T00:00:00') : '';
|
|
const tags = (e.tags || []).slice(0, 4);
|
|
|
|
const photo = e.media_url
|
|
? `<div class="diary-card-photo">
|
|
<img src="${e.media_url}" alt="Foto" loading="lazy">
|
|
</div>`
|
|
: '';
|
|
|
|
const tagsHtml = tags.length
|
|
? `<div class="diary-card-tags">${tags.map(t => `<span class="badge">${t}</span>`).join('')}</div>`
|
|
: '';
|
|
|
|
const textPreview = e.text
|
|
? `<p class="diary-card-text">${_escape(e.text.slice(0, 140))}${e.text.length > 140 ? '…' : ''}</p>`
|
|
: '';
|
|
|
|
// Meilenstein-Badge (nur bei is_milestone=1, nicht bei manuell gewähltem Typ 'meilenstein')
|
|
const milestoneBadge = e.is_milestone
|
|
? `<div class="diary-card-milestone-badge">${UI.icon('calendar-dots')} Meilenstein</div>`
|
|
: '';
|
|
|
|
// Mehrere Hunde: kleine Avatare in der Karte
|
|
const dogAvatars = _dogAvatarRow(e.dog_ids || []);
|
|
|
|
return `
|
|
<div class="diary-card${isMile ? ' diary-card--milestone' : ''}" data-entry-id="${e.id}">
|
|
${photo}
|
|
<div class="diary-card-body">
|
|
${milestoneBadge}
|
|
<div class="diary-card-meta">
|
|
<span class="diary-card-type">${typ.icon} ${typ.label}</span>
|
|
<span class="diary-card-date">${dateStr}</span>
|
|
</div>
|
|
${e.titel ? `<div class="diary-card-title">${_escape(e.titel)}</div>` : ''}
|
|
${textPreview}
|
|
${tagsHtml}
|
|
${dogAvatars}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function _dogAvatarRow(dogIds) {
|
|
if (!dogIds || dogIds.length <= 1) return '';
|
|
const avatars = dogIds.map(did => {
|
|
const dog = _appState.dogs.find(d => d.id === did);
|
|
if (!dog) return '';
|
|
return `<div class="diary-dog-av" title="${_escape(dog.name)}">
|
|
${dog.foto_url ? `<img src="${_escape(dog.foto_url)}" alt="">` : `<span>${UI.icon('dog')}</span>`}
|
|
</div>`;
|
|
}).join('');
|
|
return `<div class="diary-dog-row">${avatars}</div>`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// DETAIL-ANSICHT
|
|
// ----------------------------------------------------------
|
|
function _openDetail(entryId) {
|
|
const entry = _entries.find(e => e.id === entryId);
|
|
if (!entry) return;
|
|
|
|
const typ = TYPEN[entry.typ] || TYPEN.eintrag;
|
|
const isMile = entry.is_milestone || entry.typ === 'meilenstein';
|
|
const tags = (entry.tags || []);
|
|
|
|
const photo = entry.media_url
|
|
? `<img src="${entry.media_url}" alt="Foto"
|
|
style="width:100%;border-radius:var(--radius-md);margin-bottom:var(--space-4)">`
|
|
: '';
|
|
|
|
// Hunde-Anzeige wenn mehrere beteiligt
|
|
const dogIds = entry.dog_ids || [entry.dog_id];
|
|
const dogsHtml = dogIds.length > 1
|
|
? `<div class="diary-detail-dogs">
|
|
${dogIds.map(did => {
|
|
const dog = _appState.dogs.find(d => d.id === did);
|
|
return dog ? `<div class="diary-dog-chip">
|
|
<div class="diary-dog-av">
|
|
${dog.foto_url ? `<img src="${_escape(dog.foto_url)}" alt="">` : `<span>${UI.icon('dog')}</span>`}
|
|
</div>
|
|
<span>${_escape(dog.name)}</span>
|
|
</div>` : '';
|
|
}).join('')}
|
|
</div>`
|
|
: '';
|
|
|
|
const body = `
|
|
${isMile ? `<div class="diary-detail-milestone-badge">${UI.icon('trophy')} Meilenstein</div>` : ''}
|
|
${photo}
|
|
<div style="display:flex;gap:var(--space-2);align-items:center;margin-bottom:var(--space-3)">
|
|
<span class="badge badge-primary">${typ.icon} ${typ.label}</span>
|
|
<span style="color:var(--c-text-secondary);font-size:var(--text-sm)">
|
|
${entry.datum ? UI.time.format(entry.datum + 'T00:00:00') : ''}
|
|
</span>
|
|
</div>
|
|
${dogsHtml}
|
|
${entry.text
|
|
? `<p style="white-space:pre-wrap;line-height:1.6;color:var(--c-text)">${_escape(entry.text)}</p>`
|
|
: ''}
|
|
${tags.length
|
|
? `<div style="display:flex;flex-wrap:wrap;gap:var(--space-1);margin-top:var(--space-3)">
|
|
${tags.map(t => `<span class="badge">${t}</span>`).join('')}
|
|
</div>`
|
|
: ''}
|
|
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-5)">
|
|
<button class="btn btn-secondary flex-1" id="detail-edit">Bearbeiten</button>
|
|
<button class="btn btn-danger flex-1" id="detail-delete">Löschen</button>
|
|
</div>
|
|
`;
|
|
|
|
UI.modal.open({ title: entry.titel || typ.label, body });
|
|
|
|
document.getElementById('detail-edit')?.addEventListener('click', () => {
|
|
UI.modal.close();
|
|
_showForm(entry);
|
|
});
|
|
document.getElementById('detail-delete')?.addEventListener('click', async () => {
|
|
const ok = await UI.modal.confirm({
|
|
title: 'Eintrag löschen?',
|
|
message: 'Dieser Vorgang kann nicht rückgängig gemacht werden.',
|
|
confirmText: 'Löschen',
|
|
danger: true,
|
|
});
|
|
if (ok) {
|
|
await _deleteEntry(entryId);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// FORMULAR — Neu erstellen / Bearbeiten
|
|
// ----------------------------------------------------------
|
|
function _showForm(entry) {
|
|
const isEdit = !!entry;
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
const activeDog = _appState.activeDog;
|
|
const typOpts = Object.entries(TYPEN)
|
|
.map(([val, { icon, label }]) =>
|
|
`<option value="${val}" ${entry?.typ === val ? 'selected' : ''}>${icon} ${label}</option>`)
|
|
.join('');
|
|
|
|
// Weitere Hunde: alle außer dem aktiven
|
|
const otherDogs = _appState.dogs.filter(d => d.id !== activeDog?.id);
|
|
const entryDogIds = entry?.dog_ids || [activeDog?.id];
|
|
const dogPickerHtml = otherDogs.length > 0 ? `
|
|
<div class="form-group">
|
|
<label class="form-label">Betrifft auch</label>
|
|
<div class="diary-dog-picker">
|
|
${otherDogs.map(d => `
|
|
<label class="diary-dog-pick-item ${entryDogIds.includes(d.id) ? 'checked' : ''}">
|
|
<input type="checkbox" name="extra_dog" value="${d.id}"
|
|
${entryDogIds.includes(d.id) ? 'checked' : ''}>
|
|
<div class="diary-dog-av">
|
|
${d.foto_url ? `<img src="${_escape(d.foto_url)}" alt="">` : `<span>${UI.icon('dog')}</span>`}
|
|
</div>
|
|
<span>${_escape(d.name)}</span>
|
|
</label>`).join('')}
|
|
</div>
|
|
</div>` : '';
|
|
|
|
const body = `
|
|
<form id="diary-form" autocomplete="off">
|
|
<div class="form-group">
|
|
<label class="form-label">Typ</label>
|
|
<select class="form-control" name="typ">${typOpts}</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Datum</label>
|
|
<input class="form-control" type="date" name="datum"
|
|
value="${entry?.datum || today}" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Titel <span style="color:var(--c-text-secondary)">(optional)</span></label>
|
|
<input class="form-control" type="text" name="titel"
|
|
value="${_escape(entry?.titel || '')}" placeholder="z.B. Erster Schultag">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Text</label>
|
|
<textarea class="form-control" name="text" rows="5"
|
|
placeholder="Was ist passiert? Besonderheiten, Gedanken…">${_escape(entry?.text || '')}</textarea>
|
|
</div>
|
|
${dogPickerHtml}
|
|
<div class="form-group">
|
|
<label class="form-label" style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
|
|
<input type="checkbox" name="is_milestone" ${entry?.is_milestone ? 'checked' : ''}>
|
|
Als Meilenstein markieren
|
|
</label>
|
|
</div>
|
|
${!isEdit ? `
|
|
<div class="form-group">
|
|
<label class="form-label">Foto <span style="color:var(--c-text-secondary)">(optional)</span></label>
|
|
<input class="form-control" type="file" name="photo" accept="image/*">
|
|
<img id="diary-photo-preview" style="display:none;width:100%;max-height:200px;
|
|
object-fit:cover;border-radius:var(--radius-md);margin-top:var(--space-2)">
|
|
</div>
|
|
` : ''}
|
|
</form>
|
|
`;
|
|
|
|
const footer = `
|
|
<button type="button" class="btn btn-secondary flex-1" id="diary-form-cancel">Abbrechen</button>
|
|
<button type="submit" form="diary-form" class="btn btn-primary flex-1">
|
|
${isEdit ? 'Speichern' : 'Erstellen'}
|
|
</button>
|
|
`;
|
|
|
|
UI.modal.open({ title: isEdit ? 'Eintrag bearbeiten' : 'Neuer Eintrag', body, footer });
|
|
|
|
const form = document.getElementById('diary-form');
|
|
|
|
// Fokus auf Titel-Feld → öffnet Keyboard auf Mobile, zeigt dem User was zu tun ist
|
|
setTimeout(() => form?.querySelector('[name="titel"]')?.focus(), 150);
|
|
|
|
// Foto-Vorschau
|
|
const photoInput = form.querySelector('[name="photo"]');
|
|
const photoPreview = document.getElementById('diary-photo-preview');
|
|
if (photoInput && photoPreview) {
|
|
UI.setupPhotoPreview(photoInput, photoPreview);
|
|
photoInput.addEventListener('change', () => {
|
|
photoPreview.style.display = photoInput.files[0] ? 'block' : 'none';
|
|
});
|
|
}
|
|
|
|
document.getElementById('diary-form-cancel')?.addEventListener('click', UI.modal.close);
|
|
|
|
// Checked-Klasse auf Dog-Picker-Items toggeln
|
|
form.querySelectorAll('.diary-dog-pick-item input').forEach(cb => {
|
|
cb.addEventListener('change', () => {
|
|
cb.closest('.diary-dog-pick-item').classList.toggle('checked', cb.checked);
|
|
});
|
|
});
|
|
|
|
form.addEventListener('submit', async e => {
|
|
e.preventDefault();
|
|
const submitBtn = document.querySelector('[form="diary-form"][type="submit"]') || form.querySelector('[type="submit"]');
|
|
const fd = UI.formData(form);
|
|
|
|
// dog_ids zusammenbauen: aktiver Hund + gewählte weitere
|
|
const dogIds = [_appState.activeDog.id];
|
|
form.querySelectorAll('.diary-dog-pick-item input:checked').forEach(cb => {
|
|
const id = parseInt(cb.value);
|
|
if (!dogIds.includes(id)) dogIds.push(id);
|
|
});
|
|
|
|
await UI.asyncButton(submitBtn, async () => {
|
|
const payload = {
|
|
datum: fd.datum || null,
|
|
typ: fd.typ,
|
|
titel: fd.titel || null,
|
|
text: fd.text || null,
|
|
is_milestone: 'is_milestone' in fd,
|
|
dog_ids: dogIds,
|
|
};
|
|
|
|
if (isEdit) {
|
|
const updated = await API.diary.update(_appState.activeDog.id, entry.id, payload);
|
|
_updateEntryInList(updated);
|
|
UI.toast.success('Eintrag gespeichert.');
|
|
} else {
|
|
const created = await API.diary.create(_appState.activeDog.id, payload);
|
|
|
|
// Foto hochladen wenn vorhanden
|
|
if (photoInput?.files[0]) {
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', photoInput.files[0]);
|
|
const media = await API.diary.uploadMedia(
|
|
_appState.activeDog.id, created.id, formData
|
|
);
|
|
created.media_url = media.media_url;
|
|
} catch {
|
|
UI.toast.warning('Eintrag erstellt, Foto konnte nicht hochgeladen werden.');
|
|
}
|
|
}
|
|
|
|
_entries.unshift(created);
|
|
UI.toast.success('Eintrag erstellt.');
|
|
}
|
|
|
|
UI.modal.close();
|
|
_renderList();
|
|
});
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// EINTRAG LÖSCHEN
|
|
// ----------------------------------------------------------
|
|
async function _deleteEntry(entryId) {
|
|
try {
|
|
await API.diary.delete(_appState.activeDog.id, entryId);
|
|
_entries = _entries.filter(e => e.id !== entryId);
|
|
UI.modal.close();
|
|
_renderList();
|
|
UI.toast.success('Eintrag gelöscht.');
|
|
} catch (err) {
|
|
UI.toast.error(err.message || 'Fehler beim Löschen.');
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// HELPER
|
|
// ----------------------------------------------------------
|
|
function _updateEntryInList(updated) {
|
|
const i = _entries.findIndex(e => e.id === updated.id);
|
|
if (i !== -1) _entries[i] = updated;
|
|
}
|
|
|
|
function _formatMonth(yearMonth) {
|
|
const [y, m] = yearMonth.split('-');
|
|
return new Intl.DateTimeFormat('de-DE', { month: 'long', year: 'numeric' })
|
|
.format(new Date(+y, +m - 1, 1));
|
|
}
|
|
|
|
function _escape(str) {
|
|
if (!str) return '';
|
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// IMPORT
|
|
// ----------------------------------------------------------
|
|
function _showImport() {
|
|
UI.modal.open({
|
|
title: 'Tagebuch importieren',
|
|
body: `
|
|
<p style="color:var(--c-text-secondary);font-size:var(--text-sm);margin-bottom:var(--space-4)">
|
|
Importiere Einträge aus einer anderen App in das Tagebuch von
|
|
<strong>${_escape(_appState.activeDog?.name || 'deinem Hund')}</strong>.
|
|
</p>
|
|
|
|
<div style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
|
|
<label class="import-format-card" id="fmt-nsx">
|
|
<input type="radio" name="import-fmt" value="nsx" checked style="display:none">
|
|
<div class="import-format-icon">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note"></use></svg>
|
|
</div>
|
|
<div>
|
|
<div style="font-weight:var(--weight-semibold)">Synology NoteStation</div>
|
|
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">.nsx-Datei aus dem NoteStation-Export</div>
|
|
</div>
|
|
</label>
|
|
|
|
<label class="import-format-card" id="fmt-csv">
|
|
<input type="radio" name="import-fmt" value="csv" style="display:none">
|
|
<div class="import-format-icon">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-csv"></use></svg>
|
|
</div>
|
|
<div>
|
|
<div style="font-weight:var(--weight-semibold)">CSV / Excel</div>
|
|
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">Spalten: datum, titel, text, tags, gps_lat, gps_lon, is_milestone</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<div style="margin-top:var(--space-4)">
|
|
<label class="form-label">Datei auswählen</label>
|
|
<input type="file" class="form-control" id="import-file-input"
|
|
accept=".nsx,.csv" style="cursor:pointer">
|
|
</div>
|
|
|
|
<div id="import-result" style="display:none;margin-top:var(--space-4)"></div>`,
|
|
|
|
footer: `
|
|
<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>
|
|
<button class="btn btn-primary" id="import-start-btn">Importieren</button>`,
|
|
});
|
|
|
|
// Format-Karten klickbar machen
|
|
document.querySelectorAll('.import-format-card').forEach(card => {
|
|
card.addEventListener('click', () => {
|
|
document.querySelectorAll('.import-format-card').forEach(c => c.classList.remove('import-format-card--active'));
|
|
card.classList.add('import-format-card--active');
|
|
card.querySelector('input[type=radio]').checked = true;
|
|
// Accept-Attribut anpassen
|
|
const fmt = card.querySelector('input').value;
|
|
document.getElementById('import-file-input').accept = fmt === 'nsx' ? '.nsx' : '.csv';
|
|
});
|
|
});
|
|
// Erste Karte direkt aktiv setzen
|
|
document.getElementById('fmt-nsx')?.classList.add('import-format-card--active');
|
|
|
|
document.getElementById('import-start-btn').addEventListener('click', async () => {
|
|
const fileInput = document.getElementById('import-file-input');
|
|
const fmt = document.querySelector('input[name="import-fmt"]:checked')?.value;
|
|
const btn = document.getElementById('import-start-btn');
|
|
const resultEl = document.getElementById('import-result');
|
|
|
|
if (!fileInput.files.length) {
|
|
UI.toast('Bitte zuerst eine Datei auswählen.', 'warning');
|
|
return;
|
|
}
|
|
const file = fileInput.files[0];
|
|
const dogId = _appState.activeDog?.id;
|
|
|
|
UI.setLoading(btn, true);
|
|
resultEl.style.display = 'none';
|
|
|
|
try {
|
|
const res = fmt === 'nsx'
|
|
? await API.importData.notestation(dogId, file)
|
|
: await API.importData.csv(dogId, file);
|
|
|
|
const errHtml = res.errors?.length
|
|
? `<details style="margin-top:var(--space-2)"><summary style="font-size:var(--text-xs);cursor:pointer">${res.errors.length} Fehler anzeigen</summary>
|
|
<pre style="font-size:var(--text-xs);white-space:pre-wrap;margin-top:var(--space-1)">${_escape(res.errors.join('\n'))}</pre></details>`
|
|
: '';
|
|
|
|
resultEl.innerHTML = `
|
|
<div style="background:var(--c-success-subtle);border-radius:var(--radius-md);
|
|
padding:var(--space-3) var(--space-4);color:var(--c-success)">
|
|
<strong>${res.imported} Einträge importiert</strong>
|
|
${res.skipped ? `<span style="color:var(--c-text-muted);font-size:var(--text-sm)"> · ${res.skipped} übersprungen</span>` : ''}
|
|
${errHtml}
|
|
</div>`;
|
|
resultEl.style.display = 'block';
|
|
UI.setLoading(btn, false);
|
|
|
|
// Diary neu laden falls etwas importiert wurde
|
|
if (res.imported > 0) {
|
|
_offset = 0;
|
|
_entries = [];
|
|
await _load();
|
|
_renderList();
|
|
}
|
|
} catch (e) {
|
|
resultEl.innerHTML = `
|
|
<div style="background:var(--c-danger-subtle);border-radius:var(--radius-md);
|
|
padding:var(--space-3) var(--space-4);color:var(--c-danger)">
|
|
Fehler: ${_escape(e.message || String(e))}
|
|
</div>`;
|
|
resultEl.style.display = 'block';
|
|
UI.setLoading(btn, false);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// PUBLIC
|
|
// ----------------------------------------------------------
|
|
return { init, refresh, openNew, onDogChange };
|
|
|
|
})();
|