Feature: Tagebuch-Import (NoteStation .nsx + CSV) + Transponder in Gesundheitsdaten
- Import-Endpoint für Synology NoteStation (.nsx): HTML→Text, GPS, Bilder, Unix-Timestamp→Datum - Import-Endpoint für CSV (Komma/Semikolon, BOM-safe, DE-Datumsformat) - Import-Modal im Tagebuch mit Format-Auswahl-Karten und Ergebnis-Anzeige - Transpondernummer in Gesundheitsdaten: Anzeige + Inline-Edit via Modal - SW-Cache by-v142
This commit is contained in:
parent
6fcf841594
commit
94e0ed3daa
7 changed files with 505 additions and 5 deletions
|
|
@ -135,12 +135,20 @@ window.Page_diary = (() => {
|
|||
// ----------------------------------------------------------
|
||||
async function _renderDiary() {
|
||||
_container.innerHTML = `
|
||||
<div class="by-toolbar diary-toolbar">
|
||||
<button class="btn btn-secondary btn-sm" id="diary-import-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
|
||||
Importieren
|
||||
</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());
|
||||
|
||||
|
|
@ -547,6 +555,125 @@ window.Page_diary = (() => {
|
|||
.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
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue