Sprint 12+13: Tagebuch Day-One-Redesign, Notiz-Feature, Icon-Fixes, SW by-v405

Tagebuch:
- Day-One-Listenansicht: Wochentag + Tageszahl + Meta-Zeile (Zeit/Ort/Wetter)
- 4 Ansichten: Liste, Medien-Mosaik, Kalender (mit Sprungbuttons), Karte (GPS-Marker)
- Detail-Ansicht inline im Content-Bereich (kein Fullscreen-Overlay mehr)
- Hero-Bild vollständig sichtbar (object-fit:contain), Lightbox mit Safe-Area
- 2-Spalten-Layout Desktop: Text + Leaflet-Karte + POI-Liste
- EXIF-GPS-Extraktion bei Foto-Upload, historisches Wetter via Archive-API
- NoteStation-Import: Fotos in diary_media (80 Einträge migriert, 94 Medien)
- Stats-Endpoints: /diary/stats, /diary/calendar, /diary/locations

Notiz-Feature:
- Generische notes-Tabelle (parent_type + parent_id + meta_json)
- 📝-Button in 8 Bereichen, Notizblock-Seite mit KI-Analyse
- KI-Toggle in Einstellungen, notes_ki_enabled in User-Profil

Icons & Design:
- fill:currentColor Fix für welcome/onboarding/friends.js
- --c-icon Variable, --c-text-muted Dark Mode aufgehellt
- 15+ neue Phosphor-Icons aus lokaler Kopie
- CSS Network-First im SW, Cache-Control-Middleware

Infrastruktur:
- Wiki-Anreicherungs-Scheduler-Jobs entfernt (abgeschlossen)
- auth.py: notes_ki_enabled + is_social_media im User-Response
This commit is contained in:
rene 2026-04-25 20:44:46 +02:00
parent 95f91fdc00
commit 553e9e7854
35 changed files with 4558 additions and 370 deletions

View file

@ -963,82 +963,326 @@ html.modal-open {
}
/* ------------------------------------------------------------
12. TAGEBUCH
12. TAGEBUCH Day One Style
------------------------------------------------------------ */
/* Monats-Trennlinie */
.diary-month-header {
font-size: var(--text-sm);
font-weight: var(--weight-semibold);
color: var(--c-text-secondary);
text-transform: uppercase;
letter-spacing: 0.06em;
padding: var(--space-4) 0 var(--space-2);
border-bottom: 1px solid var(--c-border);
margin-bottom: var(--space-3);
/* Stats-Leiste */
.diary-stats-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0;
padding: 8px 12px;
border-bottom: 1px solid var(--c-divider, var(--c-border));
background: var(--c-surface);
flex-shrink: 0;
}
.diary-month-header:first-child {
padding-top: 0;
.diary-stats-numbers {
display: flex;
gap: 0;
overflow-x: auto;
scrollbar-width: none;
flex: 1;
min-width: 0;
}
.diary-stats-numbers::-webkit-scrollbar { display: none; }
.diary-stat {
display: flex;
flex-direction: column;
align-items: center;
min-width: 0;
padding: 0 8px;
border-right: 1px solid var(--c-border);
}
.diary-stat:last-child { border-right: none; }
.diary-stat-num {
font-size: 18px;
font-weight: 700;
color: var(--c-text);
line-height: 1.2;
white-space: nowrap;
}
.diary-stat-label {
font-size: 9px;
color: var(--c-text-muted);
margin-top: 2px;
white-space: nowrap;
text-transform: uppercase;
letter-spacing: .04em;
}
/* Eintragskarte */
/* View-Switcher */
.diary-view-switcher {
display: flex;
gap: 0;
flex-shrink: 0;
margin-left: 8px;
}
.diary-view-btn {
background: none;
border: none;
cursor: pointer;
padding: 5px 6px;
border-radius: 8px;
color: var(--c-text-secondary);
display: flex;
align-items: center;
transition: background .15s, color .15s;
}
.diary-view-btn:hover { background: var(--c-surface-2); color: var(--c-text); }
.diary-view-btn.active { color: var(--c-primary); background: var(--c-primary-subtle); }
.diary-view-btn .ph-icon { width: 18px; height: 18px; }
@media (min-width: 640px) {
.diary-stat { padding: 0 12px; }
.diary-stat-num { font-size: 20px; }
.diary-view-btn { padding: 6px 8px; }
.diary-view-btn .ph-icon { width: 20px; height: 20px; }
}
/* Meta-Zeile in der Karte */
.diary-meta-loc {
display: inline-flex;
align-items: center;
gap: 2px;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.diary-meta-dot { color: var(--c-text-muted); opacity: .5; }
/* Medien-Mosaic */
.diary-media-mosaic {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2px;
padding: 2px;
}
.diary-mosaic-item {
aspect-ratio: 1;
overflow: hidden;
cursor: pointer;
position: relative;
}
.diary-mosaic-item img {
width: 100%; height: 100%;
object-fit: cover;
display: block;
transition: opacity .2s;
}
.diary-mosaic-item:hover img { opacity: .85; }
/* Kalender-Ansicht */
.diary-calendar { padding: 0 0 80px; }
.diary-cal-nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 16px 8px;
}
.diary-cal-nav button {
background: none; border: none; cursor: pointer;
padding: 6px; border-radius: 8px; color: var(--c-text-muted);
display: flex; align-items: center;
}
.diary-cal-nav button:hover { background: var(--c-surface-2); }
.diary-cal-nav button .ph-icon { width: 20px; height: 20px; }
.diary-cal-month { font-size: 16px; font-weight: 600; color: var(--c-text); }
.diary-cal-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
padding: 0 8px 4px;
text-align: center;
font-size: 11px;
color: var(--c-text-muted);
font-weight: 500;
}
.diary-cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 3px;
padding: 0 8px;
}
.diary-cal-cell {
aspect-ratio: 1;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
position: relative;
overflow: hidden;
font-size: 13px;
color: var(--c-text-secondary);
}
.diary-cal-cell.has-entry {
cursor: pointer;
color: var(--c-text);
font-weight: 600;
}
.diary-cal-cell.has-entry:active { opacity: .7; }
/* Oranger Punkt unter der Tageszahl — sichtbar auch ohne Foto */
.diary-cal-cell.has-entry::after {
content: '';
display: block;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--c-primary);
flex-shrink: 0;
}
/* Foto als Hintergrund */
.diary-cal-cell.has-entry img {
position: absolute; inset: 0;
width: 100%; height: 100%;
object-fit: cover;
opacity: .4;
border-radius: 8px;
}
.diary-cal-cell.has-entry:hover img,
.diary-cal-cell.has-entry:active img { opacity: .6; }
/* Punkt ausblenden wenn Foto vorhanden (Foto reicht als Indikator) */
.diary-cal-cell.has-entry:has(img)::after { display: none; }
.diary-cal-cell.today .diary-cal-day {
background: var(--c-primary);
color: var(--c-text-inverse);
border-radius: 50%;
width: 26px; height: 26px;
display: flex; align-items: center; justify-content: center;
font-weight: 700;
}
.diary-cal-day { position: relative; z-index: 1; font-size: 13px; }
/* Monats-Section */
.diary-month-header {
font-size: 22px;
font-weight: 700;
color: var(--c-text);
padding: 12px 16px;
background: var(--c-surface-2, #f5f5f5);
margin: 0;
border-top: 1px solid var(--c-border);
border-bottom: none;
letter-spacing: -0.01em;
}
.diary-month-header:first-child {
border-top: none;
margin-top: 0;
}
@media (prefers-color-scheme: dark) {
.diary-month-header { background: var(--c-surface-2); }
}
[data-theme="dark"] .diary-month-header { background: var(--c-surface-2); }
/* Monats-Eintrags-Container (umschließt alle Karten einer Section) */
.diary-month-entries {
background: var(--c-surface);
border-bottom: 1px solid var(--c-border);
}
/* Eintragskarte — Day One Row-Style */
.diary-card {
background: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-3);
overflow: hidden;
cursor: pointer;
transition: box-shadow var(--transition-fast),
transform var(--transition-fast);
box-shadow: var(--shadow-xs);
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px 16px;
background: transparent;
border: none;
border-bottom: 1px solid var(--c-divider, var(--c-border));
border-radius: 0;
margin-bottom: 0;
overflow: visible;
cursor: pointer;
transition: background var(--transition-fast);
box-shadow: none;
-webkit-tap-highlight-color: transparent;
}
.diary-card:last-child {
border-bottom: none;
}
.diary-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-1px);
background: rgba(0,0,0,0.025);
box-shadow: none;
transform: none;
}
.diary-card:active {
transform: scale(0.99);
background: rgba(0,0,0,0.05);
transform: none;
}
[data-theme="dark"] .diary-card:hover { background: rgba(255,255,255,0.04); }
[data-theme="dark"] .diary-card:active { background: rgba(255,255,255,0.07); }
/* Datum-Spalte links */
.diary-card-date-col {
display: flex;
flex-direction: column;
align-items: center;
width: 44px;
flex-shrink: 0;
padding-top: 1px;
}
.diary-card-weekday {
font-size: 10px;
font-weight: 600;
color: var(--c-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: 1;
margin-bottom: 2px;
}
.diary-card-daynum {
font-size: 28px;
font-weight: 700;
color: var(--c-text);
line-height: 1;
}
/* Meilenstein-Icon auf der Datum-Spalte */
.diary-card-date-col .diary-milestone-icon {
font-size: 14px;
color: #c4a000;
margin-top: 4px;
}
/* Meilenstein-Hervorhebung */
.diary-card--milestone {
border-color: #d4a017;
border-width: 2px;
background: linear-gradient(
135deg,
var(--c-surface) 0%,
color-mix(in srgb, #d4a017 8%, var(--c-surface)) 100%
);
background: color-mix(in srgb, #d4a017 4%, transparent);
}
.diary-card--milestone .diary-card-daynum {
color: #b8860b;
}
/* Meilenstein-Badge innerhalb der Karte */
.diary-card-milestone-badge {
display: inline-flex;
align-items: center;
gap: 4px;
background: color-mix(in srgb, #d4a017 15%, transparent);
color: #8a6400;
font-weight: 600;
font-size: var(--text-xs);
padding: 2px var(--space-2);
border-radius: var(--radius-full);
margin-bottom: var(--space-2);
display: inline-flex;
align-items: center;
gap: 4px;
background: color-mix(in srgb, #d4a017 15%, transparent);
color: #8a6400;
font-weight: 600;
font-size: var(--text-xs);
padding: 2px var(--space-2);
border-radius: var(--radius-full);
margin-bottom: 4px;
letter-spacing: 0.03em;
}
/* Foto / Video oben */
/* Foto / Thumbnail rechts — 72×72px */
.diary-card-photo {
width: 100%;
height: 180px;
overflow: hidden;
width: 72px;
height: 72px;
flex-shrink: 0;
border-radius: 8px;
overflow: hidden;
position: relative;
margin-top: 2px;
}
.diary-card-photo img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.diary-media-picker {
display: flex;
@ -1165,7 +1409,7 @@ html.modal-open {
border-radius: 50%;
border: none;
background: rgba(0,0,0,.50);
color: #9ca3af;
color: rgba(255,255,255,.55);
font-size: 14px;
cursor: pointer;
display: flex;
@ -1177,7 +1421,7 @@ html.modal-open {
transition: color .15s, background .15s;
}
.diary-cover-btn--active {
color: #f5c518;
color: var(--c-amber);
background: rgba(0,0,0,.65);
}
.diary-cover-btn--form {
@ -1185,48 +1429,46 @@ html.modal-open {
left: var(--space-1);
}
/* Card Body */
/* Card Body — mittlere Spalte */
.diary-card-body {
padding: var(--space-3) var(--space-4);
flex: 1;
min-width: 0;
padding: 0;
}
/* Meta-Zeile: Typ + Datum */
.diary-card-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-1);
}
.diary-card-type {
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
color: var(--c-primary);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.diary-card-date {
font-size: var(--text-xs);
color: var(--c-text-secondary);
}
/* Titel */
/* Titel in Karte */
.diary-card-title {
font-size: var(--text-base);
font-weight: var(--weight-semibold);
color: var(--c-text);
margin-bottom: var(--space-1);
font-size: 15px;
font-weight: 700;
color: var(--c-text);
margin: 0 0 3px;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Meta-Zeile: nur noch für Compat — im neuen Design nicht als flex-row genutzt */
.diary-card-meta {
display: none;
}
.diary-card-type { display: none; }
.diary-card-date { display: none; }
/* Ort-Zeile in Karte */
.diary-card-location {
display: flex;
align-items: center;
gap: var(--space-1);
font-size: var(--text-sm);
color: var(--c-primary);
margin: 0 0 var(--space-1);
gap: 4px;
font-size: 12px;
color: var(--c-text-muted);
margin: 0 0 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.diary-card-location .ph-icon { flex-shrink: 0; }
.diary-card-location .ph-icon { flex-shrink: 0; width: 12px; height: 12px; }
/* Ort in Detail-Ansicht */
.diary-detail-location {
@ -1292,12 +1534,12 @@ html.modal-open {
/* Text-Vorschau */
.diary-card-text {
font-size: var(--text-sm);
font-size: 13px;
color: var(--c-text-secondary);
line-height: 1.5;
margin: 0 0 var(--space-2);
line-height: 1.45;
margin: 0 0 4px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
@ -1310,6 +1552,62 @@ html.modal-open {
margin-top: var(--space-1);
}
/* Meta-Zeile unten in der Karte: Zeit · Ort · Wetter */
.diary-card-meta-row {
font-size: 12px;
color: var(--c-text-muted);
line-height: 1.4;
margin-top: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Wetter-Badge in Karten-Meta */
.diary-weather-badge {
display: inline-flex;
align-items: center;
gap: 2px;
font-size: var(--text-xs);
color: var(--c-text-secondary);
white-space: nowrap;
}
/* FAB — Floating Action Button */
.diary-fab {
position: fixed;
bottom: calc(var(--nav-bottom-height, 64px) + env(safe-area-inset-bottom, 0px) + 16px);
right: 20px;
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--c-primary);
color: #fff;
border: none;
cursor: pointer;
box-shadow: 0 4px 16px rgba(196,132,58,.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
transition: transform .15s, box-shadow .15s;
-webkit-tap-highlight-color: transparent;
}
.diary-fab:hover { transform: scale(1.06); box-shadow: 0 6px 20px rgba(196,132,58,.5); }
.diary-fab:active { transform: scale(0.94); }
/* POI-Chips in Karte und Detail */
.diary-poi-chips,
.diary-detail-poi-chips {
font-size: var(--text-xs);
color: var(--c-text-muted);
line-height: 1.5;
margin: var(--space-1) 0 var(--space-1);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Detail-Ansicht */
.diary-detail-milestone-badge {
display: inline-flex;
@ -1324,6 +1622,312 @@ html.modal-open {
margin-bottom: var(--space-3);
}
/* Detail-View: Hero-Bild */
.diary-detail-hero {
width: 100%;
max-height: 80vh;
background: #000;
flex-shrink: 0;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
@media (min-width: 768px) {
.diary-detail-hero {
max-width: 1100px;
margin: 0 auto;
border-radius: 0 0 12px 12px;
max-height: 60vh;
}
}
@media (min-width: 1200px) {
.diary-detail-hero { max-width: 1300px; }
}
.diary-detail-hero img {
width: 100%;
height: auto;
max-height: 80vh;
object-fit: contain;
display: block;
cursor: zoom-in;
}
.diary-detail-hero video {
width: 100%;
height: auto;
max-height: 80vh;
object-fit: contain;
display: block;
background: #000;
}
/* Detail-View: inline im Content-Bereich (kein Overlay mehr) */
.diary-detail-view-inner {
display: flex;
flex-direction: column;
min-height: calc(100vh - 120px);
background: var(--c-bg);
}
/* Detail-View: Header-Bar */
.diary-detail-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--c-surface);
border-bottom: 1px solid var(--c-border);
flex-shrink: 0;
min-height: 48px;
}
.diary-detail-back {
display: flex;
align-items: center;
gap: 6px;
background: none;
border: none;
color: var(--c-primary);
font-size: 16px;
cursor: pointer;
padding: 4px 0;
font-weight: 500;
}
.diary-detail-date-center {
font-size: 14px;
font-weight: 600;
color: var(--c-text);
text-align: center;
flex: 1;
padding: 0 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.diary-detail-edit {
background: none;
border: none;
color: var(--c-primary);
cursor: pointer;
padding: 4px 0;
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
font-weight: 500;
}
/* Detail-View: Body-Wrapper (text links, Karte rechts auf Desktop) */
.diary-detail-body-wrap {
display: flex;
flex-direction: column;
flex: 1;
width: 100%;
}
@media (min-width: 768px) {
.diary-detail-body-wrap {
flex-direction: row;
align-items: flex-start;
max-width: 1100px;
margin: 0 auto;
padding: 0 24px;
box-sizing: border-box;
}
}
@media (min-width: 1200px) {
.diary-detail-body-wrap { max-width: 1300px; }
}
/* Detail-View: Inhalt */
.diary-detail-content {
padding: 24px 24px 60px;
flex: 1;
min-width: 0;
}
@media (max-width: 767px) {
.diary-detail-content { padding: 20px 16px 40px; }
}
/* Detail-View: Karte + POI-Sektion */
.diary-detail-map-wrap {
padding: 16px 16px 40px;
flex-shrink: 0;
width: 100%;
}
@media (min-width: 768px) {
.diary-detail-map-wrap {
width: 380px;
min-width: 300px;
max-width: 420px;
flex-shrink: 0;
padding: 24px 0 40px 32px;
position: sticky;
top: 60px;
align-self: flex-start;
}
}
.diary-detail-map {
width: 100%;
height: 200px;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--c-border);
margin-bottom: 12px;
}
@media (min-width: 768px) {
.diary-detail-map { height: 280px; }
}
/* POI-Liste */
.diary-detail-poi-list {
background: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: 12px;
overflow: hidden;
}
.diary-detail-poi-heading {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--c-text-muted);
padding: 10px 14px 8px;
border-bottom: 1px solid var(--c-border);
}
.diary-detail-poi-heading .ph-icon { width:14px;height:14px; }
.diary-detail-poi-row {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 14px;
border-bottom: 1px solid var(--c-border);
font-size: 13px;
}
.diary-detail-poi-row:last-child { border-bottom: none; }
.diary-detail-poi-icon { width:16px;height:16px;color:var(--c-primary);flex-shrink:0; }
.diary-detail-poi-name { flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--c-text); }
.diary-detail-poi-dist { font-size:12px;color:var(--c-text-muted);flex-shrink:0; }
.diary-detail-title {
font-size: 22px;
font-weight: 700;
color: var(--c-text);
margin: 0 0 16px;
line-height: 1.3;
}
.diary-detail-body {
font-size: 16px;
line-height: 1.7;
color: var(--c-text);
white-space: pre-wrap;
margin: 0 0 20px;
}
.diary-detail-divider {
border: none;
border-top: 1px solid var(--c-border);
margin: 20px 0;
}
/* Detail-View: Meta-Bar unten */
.diary-detail-meta-bar {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
font-size: 13px;
color: var(--c-text-muted);
margin-bottom: 16px;
align-items: center;
}
.diary-detail-meta-bar .ph-icon {
width: 14px;
height: 14px;
flex-shrink: 0;
}
.diary-detail-meta-item {
display: flex;
align-items: center;
gap: 5px;
}
/* Detail-View: Thumbnail-Strip */
.diary-detail-thumbs {
display: flex;
gap: 4px;
padding: 6px 16px;
overflow-x: auto;
background: rgba(0,0,0,.6);
flex-shrink: 0;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
@media (min-width: 768px) {
.diary-detail-thumbs {
max-width: 1100px;
margin: 0 auto;
border-radius: 0 0 8px 8px;
padding-left: 16px;
padding-right: 16px;
background: rgba(0,0,0,.75);
width: 100%;
box-sizing: border-box;
}
}
@media (min-width: 1200px) {
.diary-detail-thumbs { max-width: 1300px; }
}
.diary-detail-thumbs::-webkit-scrollbar { display: none; }
.diary-detail-thumb {
flex-shrink: 0;
width: 56px;
height: 56px;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
border: 2px solid transparent;
box-sizing: border-box;
transition: border-color .15s, opacity .15s;
opacity: .7;
}
.diary-detail-thumb:hover { opacity: 1; }
.diary-detail-thumb--active {
border-color: var(--c-primary);
opacity: 1;
}
@media (min-width: 768px) {
.diary-detail-thumb { width: 72px; height: 72px; }
}
/* Detail-View: Foto-Galerie horizontal */
.diary-detail-gallery {
display: flex;
gap: 8px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scroll-snap-type: x mandatory;
margin: 0 -20px 20px;
padding: 0 20px;
scrollbar-width: none;
}
.diary-detail-gallery::-webkit-scrollbar { display: none; }
.diary-detail-gallery-item {
flex: 0 0 auto;
width: min(75vw, 280px);
height: 200px;
border-radius: var(--radius-md);
overflow: hidden;
scroll-snap-align: start;
cursor: zoom-in;
}
.diary-detail-gallery-item img,
.diary-detail-gallery-item video {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* Leaflet-Attribution ausblenden */
.leaflet-control-attribution { display: none !important; }

View file

@ -44,6 +44,7 @@
--c-warning: #D4923A;
--c-warning-subtle: #FDF3E3;
--c-amber: #E4A020; /* Goldgelb — "Heute"-Akzent, distinct von Primary */
--c-icon: #7A6A58; /* Standard-Icon-Farbe (= text-secondary im Light-Mode) */
--c-info: #4A7A9B;
--c-info-subtle: #E8F2F8;
@ -137,8 +138,11 @@
--c-text: #F0EAE0;
--c-text-secondary: #C0B0A0;
--c-text-muted: #806A58;
--c-text-muted: #9A8878;
--c-text-inverse: #2A1F14;
--c-icon: #B0A090;
--c-amber: #C48820;
--c-success: #6A9E58;
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.30);
--shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.35), 0 1px 2px rgba(0, 0, 0, 0.25);

View file

@ -202,7 +202,7 @@
justify-content: center;
gap: 3px;
cursor: pointer;
color: var(--c-text-muted);
color: var(--c-icon, var(--c-text-secondary));
transition: color var(--transition-fast);
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;

View file

@ -30,6 +30,21 @@
<symbol id="map-trifold" viewBox="0 0 256 256"><path d="M228.92,49.69a8,8,0,0,0-6.86-1.45L160.93,63.52,99.58,32.84a8,8,0,0,0-5.52-.6l-64,16A8,8,0,0,0,24,56V200a8,8,0,0,0,9.94,7.76l61.13-15.28,61.35,30.68A8.15,8.15,0,0,0,160,224a8,8,0,0,0,1.94-.24l64-16A8,8,0,0,0,232,200V56A8,8,0,0,0,228.92,49.69ZM104,52.94l48,24V203.06l-48-24ZM40,62.25l48-12v127.5l-48,12Zm176,131.5-48,12V78.25l48-12Z"/></symbol>
<symbol id="path" viewBox="0 0 256 256"><path d="M200,168a32.06,32.06,0,0,0-31,24H72a32,32,0,0,1,0-64h96a40,40,0,0,0,0-80H72a8,8,0,0,0,0,16h96a24,24,0,0,1,0,48H72a48,48,0,0,0,0,96h97a32,32,0,1,0,31-40Zm0,48a16,16,0,1,1,16-16A16,16,0,0,1,200,216Z"/></symbol>
<symbol id="paw-print" viewBox="0 0 256 256"><path d="M212,80a28,28,0,1,0,28,28A28,28,0,0,0,212,80Zm0,40a12,12,0,1,1,12-12A12,12,0,0,1,212,120ZM72,108a28,28,0,1,0-28,28A28,28,0,0,0,72,108ZM44,120a12,12,0,1,1,12-12A12,12,0,0,1,44,120ZM92,88A28,28,0,1,0,64,60,28,28,0,0,0,92,88Zm0-40A12,12,0,1,1,80,60,12,12,0,0,1,92,48Zm72,40a28,28,0,1,0-28-28A28,28,0,0,0,164,88Zm0-40a12,12,0,1,1-12,12A12,12,0,0,1,164,48Zm23.12,100.86a35.3,35.3,0,0,1-16.87-21.14,44,44,0,0,0-84.5,0A35.25,35.25,0,0,1,69,148.82,40,40,0,0,0,88,224a39.48,39.48,0,0,0,15.52-3.13,64.09,64.09,0,0,1,48.87,0,40,40,0,0,0,34.73-72ZM168,208a24,24,0,0,1-9.45-1.93,80.14,80.14,0,0,0-61.19,0,24,24,0,0,1-20.71-43.26,51.22,51.22,0,0,0,24.46-30.67,28,28,0,0,1,53.78,0,51.27,51.27,0,0,0,24.53,30.71A24,24,0,0,1,168,208Z"/></symbol>
<symbol id="note-pencil" viewBox="0 0 256 256"><path d="M224,128v80a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V48A16,16,0,0,1,48,32h80a8,8,0,0,1,0,16H48V208H208V128a8,8,0,0,1,16,0Zm5.66-58.34-96,96A8,8,0,0,1,128,168H96a8,8,0,0,1-8-8V128a8,8,0,0,1,2.34-5.66l96-96a8,8,0,0,1,11.32,0l32,32A8,8,0,0,1,229.66,69.66Zm-17-5.66L192,43.31,179.31,56,200,76.69Z"/></symbol>
<symbol id="images" viewBox="0 0 256 256"><path d="M216,40H72A16,16,0,0,0,56,56V72H40A16,16,0,0,0,24,88V200a16,16,0,0,0,16,16H184a16,16,0,0,0,16-16V184h16a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40ZM172,72a12,12,0,1,1-12,12A12,12,0,0,1,172,72Zm12,128H40V88H56v80a16,16,0,0,0,16,16H184Zm32-32H72V120.69l30.34-30.35a8,8,0,0,1,11.32,0L163.31,140,189,114.34a8,8,0,0,1,11.31,0L216,130.07V168Z"/></symbol>
<symbol id="caret-left" viewBox="0 0 256 256"><path d="M163.06,40.61a8,8,0,0,0-8.72,1.73l-80,80a8,8,0,0,0,0,11.32l80,80A8,8,0,0,0,168,208V48A8,8,0,0,0,163.06,40.61Z"/></symbol>
<symbol id="caret-right" viewBox="0 0 256 256"><path d="M181.66,122.34l-80-80A8,8,0,0,0,88,48V208a8,8,0,0,0,13.66,5.66l80-80A8,8,0,0,0,181.66,122.34Z"/></symbol>
<symbol id="coffee" viewBox="0 0 256 256"><path d="M208,80H32a8,8,0,0,0-8,8v48a96.3,96.3,0,0,0,32.54,72H32a8,8,0,0,0,0,16H208a8,8,0,0,0,0-16H183.46a96.59,96.59,0,0,0,27-40.09A40,40,0,0,0,248,128v-8A40,40,0,0,0,208,80Zm24,48a24,24,0,0,1-17.2,23,95.78,95.78,0,0,0,1.2-15V97.38A24,24,0,0,1,232,120ZM112,56V24a8,8,0,0,1,16,0V56a8,8,0,0,1-16,0Zm32,0V24a8,8,0,0,1,16,0V56a8,8,0,0,1-16,0ZM80,56V24a8,8,0,0,1,16,0V56a8,8,0,0,1-16,0Z"/></symbol>
<symbol id="beer-bottle" viewBox="0 0 256 256"><path d="M245.66,42.34l-32-32a8,8,0,0,0-11.32,11.32l1.48,1.47L148.65,64.51l-38.22,7.65a8.05,8.05,0,0,0-4.09,2.18L23,157.66a24,24,0,0,0,0,33.94L64.4,233a24,24,0,0,0,33.94,0l83.32-83.31a8,8,0,0,0,2.18-4.09l7.65-38.22,41.38-55.17,1.47,1.48a8,8,0,0,0,11.32-11.32ZM81.37,224a7.94,7.94,0,0,1-5.65-2.34L34.34,180.28a8,8,0,0,1,0-11.31L40,163.31,92.69,216,87,221.66A8,8,0,0,1,81.37,224ZM177.6,99.2a7.92,7.92,0,0,0-1.44,3.23l-7.53,37.63L160,148.69,107.31,96l8.63-8.63,37.63-7.53a7.92,7.92,0,0,0,3.23-1.44l58.45-43.84,6.19,6.19Z"/></symbol>
<symbol id="shopping-bag" viewBox="0 0 256 256"><path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm-88,96A48.05,48.05,0,0,1,80,88a8,8,0,0,1,16,0,32,32,0,0,0,64,0,8,8,0,0,1,16,0A48.05,48.05,0,0,1,128,136Z"/></symbol>
<symbol id="binoculars" viewBox="0 0 256 256"><path d="M237.22,151.9l0-.1a1.42,1.42,0,0,0-.07-.22,48.46,48.46,0,0,0-2.31-5.3L193.27,51.8a8,8,0,0,0-1.67-2.44,32,32,0,0,0-45.26,0A8,8,0,0,0,144,55V80H112V55a8,8,0,0,0-2.34-5.66,32,32,0,0,0-45.26,0,8,8,0,0,0-1.67,2.44L21.2,146.28a48.46,48.46,0,0,0-2.31,5.3,1.72,1.72,0,0,0-.07.21s0,.08,0,.11a48,48,0,0,0,90.32,32.51,47.49,47.49,0,0,0,2.9-16.59V96h32v71.83a47.49,47.49,0,0,0,2.9,16.59,48,48,0,0,0,90.32-32.51Zm-143.15,27a32,32,0,0,1-60.2-21.71l1.81-4.13A32,32,0,0,1,96,167.88V168h0A32,32,0,0,1,94.07,178.94ZM203,198.07A32,32,0,0,1,160,168h0v-.11a32,32,0,0,1,60.32-14.78l1.81,4.13A32,32,0,0,1,203,198.07Z"/></symbol>
<symbol id="buildings" viewBox="0 0 256 256"><path d="M239.73,208H224V96a16,16,0,0,0-16-16H164a4,4,0,0,0-4,4V208H144V32.41a16.43,16.43,0,0,0-6.16-13,16,16,0,0,0-18.72-.69L39.12,72A16,16,0,0,0,32,85.34V208H16.27A8.18,8.18,0,0,0,8,215.47,8,8,0,0,0,16,224H240a8,8,0,0,0,8-8.53A8.18,8.18,0,0,0,239.73,208ZM76,184a8,8,0,0,1-8.53,8A8.18,8.18,0,0,1,60,183.72V168.27A8.19,8.19,0,0,1,67.47,160,8,8,0,0,1,76,168Zm0-56a8,8,0,0,1-8.53,8A8.19,8.19,0,0,1,60,127.72V112.27A8.19,8.19,0,0,1,67.47,104,8,8,0,0,1,76,112Zm40,56a8,8,0,0,1-8.53,8,8.18,8.18,0,0,1-7.47-8.26V168.27a8.19,8.19,0,0,1,7.47-8.26,8,8,0,0,1,8.53,8Zm0-56a8,8,0,0,1-8.53,8,8.19,8.19,0,0,1-7.47-8.26V112.27a8.19,8.19,0,0,1,7.47-8.26,8,8,0,0,1,8.53,8Z"/></symbol>
<symbol id="bed" viewBox="0 0 256 256"><path d="M216,72H32V48a8,8,0,0,0-16,0V208a8,8,0,0,0,16,0V176H240v32a8,8,0,0,0,16,0V112A40,40,0,0,0,216,72ZM32,88h72v72H32Z"/></symbol>
<symbol id="church" viewBox="0 0 256 256"><path d="M228.12,145.14,192,123.47V104a8,8,0,0,0-4-7L136,67.36V48h16a8,8,0,0,0,0-16H136V16a8,8,0,0,0-16,0V32H104a8,8,0,0,0,0,16h16V67.36L68,97.05a8,8,0,0,0-4,7v19.47L27.88,145.14A8,8,0,0,0,24,152v64a8,8,0,0,0,8,8h72a8,8,0,0,0,8-8V168a16,16,0,0,1,32,0v48a8,8,0,0,0,8,8h72a8,8,0,0,0,8-8V152A8,8,0,0,0,228.12,145.14ZM64,208H40V156.53l24-14.4Zm152,0H192V142.13l24,14.4Z"/></symbol>
<symbol id="soccer-ball" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm8,39.38,24.79-17.05a88.41,88.41,0,0,1,36.18,27l-8,26.94c-.2,0-.41.1-.61.17l-22.82,7.41a7.59,7.59,0,0,0-1,.4L136,88.62c0-.2,0-.41,0-.62V64C136,63.79,136,63.58,136,63.38ZM95.24,46.33,120,63.38c0,.2,0,.41,0,.62V88c0,.21,0,.42,0,.62L91.44,108.29a7.59,7.59,0,0,0-1-.4l-22.82-7.41c-.2-.07-.41-.12-.61-.17l-8-26.94A88.41,88.41,0,0,1,95.24,46.33Zm-13,129.09H53.9a87.4,87.4,0,0,1-13.79-43.07l22-16.88a5.77,5.77,0,0,0,.58.22l22.83,7.42a7.83,7.83,0,0,0,.93.22l10.79,31.42c-.15.18-.3.36-.44.55L82.7,174.71A7.8,7.8,0,0,0,82.24,175.42ZM150.69,213a88.16,88.16,0,0,1-45.38,0L95.25,184.6c.13-.16.27-.31.39-.48l14.11-19.42a7.66,7.66,0,0,0,.46-.7h35.58a7.66,7.66,0,0,0,.46.7l14.11,19.42c.12.17.26.32.39.48Zm23.07-37.61a7.8,7.8,0,0,0-.46-.71L159.19,155.3c-.14-.19-.29-.37-.44-.55l10.79-31.42a7.83,7.83,0,0,0,.93-.22l22.83-7.42a5.77,5.77,0,0,0,.58-.22l22,16.88a87.4,87.4,0,0,1-13.79,43.07Z"/></symbol>
<symbol id="tree" viewBox="0 0 256 256"><path d="M128,187.85a72.44,72.44,0,0,0,8,4.62V232a8,8,0,0,1-16,0V192.47A72.44,72.44,0,0,0,128,187.85ZM198.1,62.59a76,76,0,0,0-140.2,0A71.71,71.71,0,0,0,16,127.8C15.9,166,48,199,86.14,200A72.22,72.22,0,0,0,120,192.47V156.94L76.42,135.16a8,8,0,1,1,7.16-14.32L120,139.06V88a8,8,0,0,1,16,0v27.06l36.42-18.22a8,8,0,1,1,7.16,14.32L136,132.94v59.53A72.17,72.17,0,0,0,168,200l1.82,0C208,199,240.11,166,240,127.8A71.71,71.71,0,0,0,198.1,62.59Z"/></symbol>
<symbol id="caret-double-left" viewBox="0 0 256 256"><path d="M203.06,40.61a8,8,0,0,0-8.72,1.73L128,108.69V48a8,8,0,0,0-13.66-5.66l-80,80a8,8,0,0,0,0,11.32l80,80A8,8,0,0,0,128,208V147.31l66.34,66.35A8,8,0,0,0,208,208V48A8,8,0,0,0,203.06,40.61Z"/></symbol>
<symbol id="caret-double-right" viewBox="0 0 256 256"><path d="M221.66,122.34l-80-80A8,8,0,0,0,128,48v60.69L61.66,42.34A8,8,0,0,0,48,48V208a8,8,0,0,0,13.66,5.66L128,147.31V208a8,8,0,0,0,13.66,5.66l80-80A8,8,0,0,0,221.66,122.34Z"/></symbol>
<symbol id="pencil-simple" viewBox="0 0 256 256"><path d="M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z"/></symbol>
<symbol id="plus" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"/></symbol>
<symbol id="spinner" viewBox="0 0 256 256"><path d="M136,32V64a8,8,0,0,1-16,0V32a8,8,0,0,1,16,0Zm37.25,58.75a8,8,0,0,0,5.66-2.35l22.63-22.62a8,8,0,0,0-11.32-11.32L167.6,77.09a8,8,0,0,0,5.65,13.66ZM224,120H192a8,8,0,0,0,0,16h32a8,8,0,0,0,0-16Zm-45.09,47.6a8,8,0,0,0-11.31,11.31l22.62,22.63a8,8,0,0,0,11.32-11.32ZM128,184a8,8,0,0,0-8,8v32a8,8,0,0,0,16,0V192A8,8,0,0,0,128,184ZM77.09,167.6,54.46,190.22a8,8,0,0,0,11.32,11.32L88.4,178.91A8,8,0,0,0,77.09,167.6ZM72,128a8,8,0,0,0-8-8H32a8,8,0,0,0,0,16H64A8,8,0,0,0,72,128ZM65.78,54.46A8,8,0,0,0,54.46,65.78L77.09,88.4A8,8,0,0,0,88.4,77.09Z"/></symbol>

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Before After
Before After

View file

@ -88,9 +88,9 @@
</script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css">
<link rel="stylesheet" href="/css/layout.css?v=237">
<link rel="stylesheet" href="/css/components.css?v=232">
<link rel="stylesheet" href="/css/design-system.css?v=382">
<link rel="stylesheet" href="/css/layout.css?v=382">
<link rel="stylesheet" href="/css/components.css?v=382">
</head>
<body>
@ -124,6 +124,9 @@
<div class="sidebar-item" data-page="trainingsplaene">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#clipboard-text"></use></svg> Trainingspläne
</div>
<div class="sidebar-item" data-page="notes">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notizblock
</div>
<span class="sidebar-section-label">Entdecken</span>
<div class="sidebar-item" data-page="map">
@ -371,6 +374,10 @@
<div class="page-body page-container"></div>
</section>
<section class="page" id="page-notes">
<div class="page-body page-container"></div>
</section>
</main>
<!-- MOBILE BOTTOM NAVIGATION -->

View file

@ -118,6 +118,7 @@ const API = (() => {
const q = new URLSearchParams(params).toString();
return get(`/dogs/${dogId}/diary${q ? '?' + q : ''}`);
},
stats(dogId) { return get(`/dogs/${dogId}/diary/stats`); },
get(dogId, entryId) { return get(`/dogs/${dogId}/diary/${entryId}`); },
create(dogId, data) { return post(`/dogs/${dogId}/diary`, data); },
update(dogId, id, data){ return patch(`/dogs/${dogId}/diary/${id}`, data); },
@ -137,6 +138,8 @@ const API = (() => {
nearby(dogId, lat, lon) {
return get(`/dogs/${dogId}/diary/nearby?lat=${lat}&lon=${lon}`);
},
locations(dogId) { return get(`/dogs/${dogId}/diary/locations`); },
calendar(dogId) { return get(`/dogs/${dogId}/diary/calendar`); },
};
// ----------------------------------------------------------
@ -559,6 +562,30 @@ const API = (() => {
},
};
// ----------------------------------------------------------
// NOTIZEN
// ----------------------------------------------------------
const notes = {
get(parentType, parentId) {
return get(`/notes/${parentType}/${parentId}`);
},
getAll(params) {
return get('/notes?' + new URLSearchParams(params || {}).toString());
},
analyse() {
return post('/notes/ki-analyse', {});
},
create(parentType, parentId, data) {
return post(`/notes/${parentType}/${parentId}`, data);
},
update(id, data) {
return patch(`/notes/${id}`, data);
},
delete(id) {
return del(`/notes/${id}`);
},
};
// ----------------------------------------------------------
// ERROR-KLASSE
// ----------------------------------------------------------
@ -576,7 +603,7 @@ const API = (() => {
get, post, put, patch, del, upload,
auth, dogs, diary, health, tieraerzte, poison,
places, routes, walks, events, sitting, forum, lost, knigge, weather, push,
friends, chat, webcal, importData, sharing, widget, notifications, services, ratings, sittingAccess, training,
friends, chat, webcal, importData, sharing, widget, notifications, services, ratings, sittingAccess, training, notes,
subscribeToPush, getLocation,
APIError,
};

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '351'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '385'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const App = (() => {

File diff suppressed because it is too large Load diff

View file

@ -28,6 +28,10 @@ window.Page_dog_profile = (() => {
if (e.target.closest('#profile-goto-login')) {
App.navigate('settings');
}
if (e.target.closest('[data-action="goto-weight"]')) {
App.navigate('health', true, { tab: 'gewicht', openForm: true });
return;
}
});
await _render();
@ -119,7 +123,7 @@ window.Page_dog_profile = (() => {
</div>
` : ''}
${dog.gewicht_kg ? `
<div class="card" style="padding:var(--space-3)">
<div class="card" style="padding:var(--space-3);cursor:pointer" data-action="goto-weight">
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
margin-bottom:2px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#scales"></use></svg> Gewicht</div>
<div style="font-weight:500;font-size:var(--text-sm)">${dog.gewicht_kg} kg</div>

View file

@ -186,7 +186,7 @@ window.Page_erste_hilfe = (() => {
<th style="padding:var(--space-2) var(--space-3);text-align:left;font-weight:var(--weight-semibold)">Bedeutung</th>
</tr></thead>
<tbody>
<tr><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border)">Rosa, feucht</td><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border);color:#22c55e;font-weight:var(--weight-semibold)">Normal</td></tr>
<tr><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border)">Rosa, feucht</td><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border);color:var(--c-success);font-weight:var(--weight-semibold)">Normal</td></tr>
<tr style="background:var(--c-surface-2)"><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border)">Blass / weiß</td><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border);color:var(--c-danger)">Schock, Blutverlust, Vergiftung</td></tr>
<tr><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border)">Blau / grau</td><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border);color:var(--c-danger);font-weight:var(--weight-semibold)">Sauerstoffmangel NOTFALL</td></tr>
<tr style="background:var(--c-surface-2)"><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border)">Gelb</td><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border);color:var(--c-warning)">Leberprobleme</td></tr>
@ -239,6 +239,7 @@ window.Page_erste_hilfe = (() => {
`;
_bindTabs();
_bindAccordions();
_bindNoteButtons();
_activateTab('lebensgefahr');
}
@ -340,6 +341,10 @@ window.Page_erste_hilfe = (() => {
${massnahmenHtml}
${warnHtml}
${e.extra || ''}
<div style="margin-top:var(--space-3);text-align:right">
<button class="btn btn-ghost btn-xs eh-note-btn" style="font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 8px"
data-kat-id="${katId}" data-titel="${e.titel.replace(/"/g,'&quot;')}"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div>
</div>
</div>
`;
@ -382,6 +387,102 @@ window.Page_erste_hilfe = (() => {
});
}
function _bindNoteButtons() {
_container.querySelectorAll('.eh-note-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const katId = btn.dataset.katId;
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);
});
});
}
// ----------------------------------------------------------------
// 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;') : '';
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">${_esc(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
// ----------------------------------------------------------------

View file

@ -226,7 +226,14 @@ window.Page_events = (() => {
</a>
</div>` : ''}
</div>
${isOwn ? `<button class="btn-icon" data-ev-edit="${ev.id}" title="Bearbeiten">${_icon('pencil-simple')}</button>` : ''}
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:var(--space-1)">
${isOwn ? `<button class="btn-icon" data-ev-edit="${ev.id}" title="Bearbeiten" onclick="event.stopPropagation()">${_icon('pencil-simple')}</button>` : ''}
${_state.user ? `<button class="btn-icon ev-note-btn" data-ev-note-id="${ev.id}"
data-ev-note-label="${UI.escape(ev.titel + ' ' + ev.datum)}"
data-ev-note-ort="${UI.escape(ev.ort_name || '')}"
title="Notiz" style="color:var(--c-text-muted)" onclick="event.stopPropagation()">
${_icon('note-pencil')}</button>` : ''}
</div>
</div>
`;
}
@ -268,7 +275,7 @@ window.Page_events = (() => {
const popup = `
<div style="min-width:180px">
<strong>${UI.escape(ev.titel)}</strong><br>
<span style="color:#666;font-size:12px">${datum}</span><br>
<span style="color:var(--c-text-muted);font-size:12px">${datum}</span><br>
${ev.ort_name ? `<span style="font-size:12px">📍 ${UI.escape(ev.ort_name)}</span><br>` : ''}
${ev.beschreibung ? `<span style="font-size:12px">${UI.escape(ev.beschreibung.slice(0, 80))}${ev.beschreibung.length > 80 ? '…' : ''}</span><br>` : ''}
<a href="#" onclick="event.preventDefault();Page_events._openDetail(${ev.id})"
@ -634,11 +641,77 @@ window.Page_events = (() => {
return;
}
// Notiz-Button
const noteBtn = e.target.closest('.ev-note-btn');
if (noteBtn) {
e.stopPropagation();
_openNoteModal(
'event',
parseInt(noteBtn.dataset.evNoteId),
noteBtn.dataset.evNoteLabel,
noteBtn.dataset.evNoteOrt || null
);
return;
}
// Karten-Klick → Detail
const card = e.target.closest('[data-ev-id]');
if (card) { _showDetail(parseInt(card.dataset.evId)); }
}
// ----------------------------------------------------------
// 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" style="color:var(--c-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 };
})();

View file

@ -44,7 +44,7 @@ window.Page_friends = (() => {
<div style="width:36px;height:36px;border-radius:50%;flex-shrink:0;
background:var(--c-primary-subtle);
display:flex;align-items:center;justify-content:center">
<svg style="width:18px;height:18px;color:var(--c-primary)" aria-hidden="true">
<svg style="fill:currentColor;width:18px;height:18px;color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#link"></use>
</svg>
</div>

View file

@ -34,10 +34,17 @@ window.Page_health = (() => {
// ----------------------------------------------------------
// LIFECYCLE
// ----------------------------------------------------------
async function init(container, appState) {
async function init(container, appState, params) {
_container = container;
_appState = appState;
if (params?.tab) {
const valid = _getTabs().some(t => t.key === params.tab);
if (valid) _activeTab = params.tab;
}
await _render();
if (params?.openForm) {
setTimeout(() => _showForm(null, _activeTab), 200);
}
}
async function refresh() {
@ -400,6 +407,10 @@ window.Page_health = (() => {
Nächste Impfung: ${UI.time.format(e.naechstes + 'T00:00:00')} ${ampel.icon}
</div>` : ''}
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
data-action="open-note" data-entry-id="${e.id}"
data-label="${_esc(e.bezeichnung)}"
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div>
</div>
`;
@ -445,6 +456,10 @@ window.Page_health = (() => {
</div>` : ''}
${e.diagnose ? `<div class="health-card-note"><b>Diagnose:</b> ${_esc(e.diagnose)}</div>` : ''}
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
data-action="open-note" data-entry-id="${e.id}"
data-label="${_esc(e.bezeichnung)}"
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div>
</div>
`;
@ -493,6 +508,10 @@ window.Page_health = (() => {
</span>
</div>
${e.notiz ? `<div class="health-card-note" style="padding-top:var(--space-1)">${_esc(e.notiz)}</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-1);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
data-action="open-note" data-entry-id="${e.id}"
data-label="Gewicht ${_esc(e.datum)}"
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div>
`).join('');
@ -726,6 +745,10 @@ window.Page_health = (() => {
${interval ? ` · Abstand zur Vorherigen: ${interval} Tage` : ''}
</div>
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
data-action="open-note" data-entry-id="${e.id}"
data-label="Läufigkeit ${_esc(e.datum)}"
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div>
</div>`;
}).join('');
@ -760,6 +783,10 @@ window.Page_health = (() => {
${e.bis_datum ? ` · bis ${UI.time.format(e.bis_datum + 'T00:00:00')}` : ''}
</div>
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
data-action="open-note" data-entry-id="${e.id}"
data-label="${_esc(e.bezeichnung)}"
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div>
</div>
`).join('')}
@ -797,6 +824,10 @@ window.Page_health = (() => {
</div>
${e.reaktion ? `<div class="health-card-note"><b>Reaktion:</b> ${_esc(e.reaktion)}</div>` : ''}
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
data-action="open-note" data-entry-id="${e.id}"
data-label="${_esc(e.bezeichnung)}"
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div>
</div>
`).join('');
@ -837,6 +868,10 @@ window.Page_health = (() => {
${count > 1 ? ` · ${count} Dateien` : ''}
</div>
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
data-action="open-note" data-entry-id="${e.id}"
data-label="${_esc(e.bezeichnung)}"
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
${count
? `<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2);align-items:center;flex-wrap:wrap">
${mediaList.slice(0, 3).map(m => m.media_type === 'pdf'
@ -874,6 +909,14 @@ window.Page_health = (() => {
const entry = (_data[_activeTab] || []).find(e => e.id === id);
if (entry) card.addEventListener('click', () => _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);
});
});
// Praxis öffnen
content.querySelectorAll('[data-action="open-praxis"]').forEach(el => {
el.addEventListener('click', () => {
@ -1166,6 +1209,9 @@ window.Page_health = (() => {
if (!_data[t]) _data[t] = [];
_data[t].unshift(saved);
UI.toast.success('Eintrag erstellt.');
if (t === 'gewicht' && saved.wert) {
_appState.activeDog.gewicht_kg = saved.wert;
}
}
// Multi-File-Upload
@ -1830,6 +1876,89 @@ window.Page_health = (() => {
.replace(/"/g, '&quot;');
}
// ----------------------------------------------------------
// 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">${_esc(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);
}
});
}
return { init, refresh, openNew, onDogChange };
})();

View file

@ -0,0 +1,693 @@
/* ============================================================
BAN YARO Notizblock
Seiten-Modul: Alle Notizen mit Filter, Suche, Sortierung und KI-Analyse.
============================================================ */
window.Page_notes = (() => {
let _container = null;
let _appState = null;
let _notes = [];
// Aktueller Filter-/Such-Zustand
let _filterType = ''; // '' = alle
let _sortMode = 'newest'; // newest | type | location
let _searchQ = '';
let _searchTimer = null;
// KI-Panel
let _kiOpen = false;
let _kiLoading = false;
let _kiSuggestions = null;
let _kiError = null;
// ----------------------------------------------------------
// Rubrik-Konfiguration
// ----------------------------------------------------------
const RUBRIKEN = [
{ type: '', label: 'Alle', color: 'var(--c-text-muted)', icon: 'note' },
{ type: 'health', label: 'Gesundheit', color: '#e74c3c', icon: 'heart' },
{ type: 'diary', label: 'Tagebuch', color: '#C4843A', icon: 'book-open' },
{ type: 'training_session', label: 'Training', color: '#27ae60', icon: 'target' },
{ type: 'route', label: 'Routen', color: '#2980b9', icon: 'path' },
{ type: 'event', label: 'Events', color: '#8e44ad', icon: 'calendar' },
{ type: 'walk', label: 'Gassi-Treffen',color: '#f39c12', icon: 'paw-print' },
{ type: 'sitting', label: 'Sitting', color: '#16a085', icon: 'house-line' },
{ type: 'erste_hilfe', label: 'Erste Hilfe', color: '#c0392b', icon: 'first-aid' },
];
function _rubrik(type) {
return RUBRIKEN.find(r => r.type === type) || { type, label: type, color: 'var(--c-text-muted)', icon: 'note' };
}
// ----------------------------------------------------------
// Hilfsfunktionen
// ----------------------------------------------------------
function _esc(s) {
if (!s) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function _formatTime(isoStr) {
if (!isoStr) return '';
try {
const d = new Date(isoStr.replace(' ', 'T') + (isoStr.includes('T') || isoStr.endsWith('Z') ? '' : 'Z'));
return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
} catch (_) { return ''; }
}
function _dateGroup(isoStr) {
if (!isoStr) return 'Älteres';
try {
const d = new Date(isoStr.replace(' ', 'T') + (isoStr.includes('T') || isoStr.endsWith('Z') ? '' : 'Z'));
const now = new Date();
const diffDays = (now - d) / 86400000;
if (diffDays < 1 && d.getDate() === now.getDate()) return 'Heute';
if (diffDays < 7) return 'Diese Woche';
return 'Älteres';
} catch (_) { return 'Älteres'; }
}
function _truncate(str, max = 150) {
if (!str) return '';
return str.length > max ? str.slice(0, max) + '…' : str;
}
// ----------------------------------------------------------
// Daten laden
// ----------------------------------------------------------
async function _load() {
const params = {};
if (_filterType) params.parent_type = _filterType;
if (_sortMode !== 'newest') params.sort = _sortMode;
if (_searchQ) params.q = _searchQ;
return await API.notes.getAll(params);
}
// ----------------------------------------------------------
// Filter/Sortierung anwenden (client-seitig falls API alles zurückgibt)
// ----------------------------------------------------------
function _applySort(list) {
const copy = [...list];
if (_sortMode === 'newest') {
copy.sort((a, b) => new Date(b.updated_at || b.created_at) - new Date(a.updated_at || a.created_at));
} else if (_sortMode === 'type') {
copy.sort((a, b) => (a.parent_type || '').localeCompare(b.parent_type || '', 'de'));
} else if (_sortMode === 'location') {
copy.sort((a, b) => (a.location_name || '').localeCompare(b.location_name || '', 'de'));
}
return copy;
}
// ----------------------------------------------------------
// Rendern
// ----------------------------------------------------------
function _render() {
const kiEnabled = _appState?.user?.notes_ki_enabled !== 0;
const sorted = _applySort(_notes);
// Gruppen aufbauen
const groups = { 'Heute': [], 'Diese Woche': [], 'Älteres': [] };
sorted.forEach(n => {
const g = _dateGroup(n.updated_at || n.created_at);
groups[g].push(n);
});
const groupHtml = Object.entries(groups)
.filter(([, items]) => items.length > 0)
.map(([label, items]) => `
<div class="notes-group">
<div class="notes-group-label">${_esc(label)}</div>
${items.map(_noteCard).join('')}
</div>
`).join('');
_container.innerHTML = `
<div class="notes-page">
<!-- Header -->
<div class="notes-header">
<h2 class="notes-title">Notizblock</h2>
<span class="notes-count">${_notes.length} Notiz${_notes.length !== 1 ? 'en' : ''}</span>
</div>
<!-- KI-Panel -->
${kiEnabled ? _kiPanelHtml() : ''}
<!-- Filter-Chips -->
<div class="notes-filter-chips">
${RUBRIKEN.map(r => `
<button class="notes-chip ${_filterType === r.type ? 'notes-chip--active' : ''}"
data-type="${_esc(r.type)}"
style="${_filterType === r.type ? `--chip-color:${r.color}` : ''}">
${_esc(r.label)}
</button>
`).join('')}
</div>
<!-- Suche + Sortierung -->
<div class="notes-toolbar">
<div class="notes-search-wrap">
<i class="ph ph-magnifying-glass notes-search-icon"></i>
<input id="notes-search" type="search" class="notes-search-input"
placeholder="Suche…" value="${_esc(_searchQ)}">
</div>
<div class="notes-sort-btns">
<button class="notes-sort-btn ${_sortMode === 'newest' ? 'notes-sort-btn--active' : ''}"
data-sort="newest">Neueste</button>
<button class="notes-sort-btn ${_sortMode === 'type' ? 'notes-sort-btn--active' : ''}"
data-sort="type">Rubrik</button>
<button class="notes-sort-btn ${_sortMode === 'location' ? 'notes-sort-btn--active' : ''}"
data-sort="location">Ort</button>
</div>
</div>
<!-- Liste -->
<div class="notes-list">
${sorted.length === 0
? UI.emptyState({ icon: 'note', title: 'Keine Notizen', text: 'Füge Notizen zu Trainingseinheiten oder anderen Einträgen hinzu.' })
: groupHtml
}
</div>
</div>
<style>
.notes-page { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-3); }
.notes-header { display: flex; align-items: center; justify-content: space-between; }
.notes-title { font-size: var(--text-lg); font-weight: var(--weight-bold); color: var(--c-text); margin: 0; }
.notes-count { font-size: var(--text-xs); color: var(--c-text-muted); }
/* KI-Panel */
.notes-ki-panel { background: var(--c-surface-2); border: 1.5px solid var(--c-border); border-radius: var(--radius-lg); overflow: hidden; }
.notes-ki-header { display: flex; align-items: center; justify-content: space-between; padding: var(--space-3) var(--space-4); cursor: pointer; gap: var(--space-2); }
.notes-ki-header-left { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--c-text); }
.notes-ki-chevron { transition: transform .2s; color: var(--c-text-muted); }
.notes-ki-chevron--open { transform: rotate(180deg); }
.notes-ki-body { padding: var(--space-3) var(--space-4) var(--space-4); border-top: 1px solid var(--c-border); }
.notes-ki-btn { padding: var(--space-2) var(--space-4); border-radius: var(--radius-md); border: none; background: var(--c-primary); color: #fff; font-size: var(--text-sm); font-weight: var(--weight-semibold); cursor: pointer; }
.notes-ki-btn:disabled { opacity: .6; cursor: default; }
.notes-ki-suggestions { margin-top: var(--space-3); font-size: var(--text-sm); color: var(--c-text); line-height: 1.6; }
.notes-ki-suggestions ul { margin: var(--space-2) 0 0; padding-left: var(--space-4); }
.notes-ki-suggestions li { margin-bottom: var(--space-1); }
.notes-ki-error { margin-top: var(--space-2); font-size: var(--text-sm); color: var(--c-danger); }
/* Filter-Chips */
.notes-filter-chips { display: flex; gap: var(--space-2); overflow-x: auto; padding-bottom: 2px; scrollbar-width: none; }
.notes-filter-chips::-webkit-scrollbar { display: none; }
.notes-chip { flex-shrink: 0; font-size: var(--text-xs); font-weight: var(--weight-semibold); padding: 4px var(--space-3); border-radius: 999px; border: 1.5px solid var(--c-border); background: var(--c-surface-2); color: var(--c-text-secondary); cursor: pointer; white-space: nowrap; transition: background .15s, color .15s, border-color .15s; }
.notes-chip--active { background: var(--chip-color, var(--c-primary)); color: #fff; border-color: var(--chip-color, var(--c-primary)); }
/* Toolbar */
.notes-toolbar { display: flex; gap: var(--space-2); align-items: center; }
.notes-search-wrap { position: relative; flex: 1; }
.notes-search-icon { position: absolute; left: var(--space-3); top: 50%; transform: translateY(-50%); color: var(--c-text-muted); font-size: 1rem; pointer-events: none; }
.notes-search-input { width: 100%; padding: var(--space-2) var(--space-3) var(--space-2) calc(var(--space-3) + 1.3rem); border: 1.5px solid var(--c-border); border-radius: var(--radius-md); font-size: var(--text-sm); background: var(--c-surface); color: var(--c-text); outline: none; box-sizing: border-box; }
.notes-search-input:focus { border-color: var(--c-primary); }
.notes-sort-btns { display: flex; border: 1.5px solid var(--c-border); border-radius: var(--radius-md); overflow: hidden; flex-shrink: 0; }
.notes-sort-btn { padding: var(--space-2) var(--space-3); font-size: var(--text-xs); font-weight: var(--weight-semibold); border: none; background: var(--c-surface-2); color: var(--c-text-secondary); cursor: pointer; transition: background .15s, color .15s; border-right: 1px solid var(--c-border); }
.notes-sort-btn:last-child { border-right: none; }
.notes-sort-btn--active { background: var(--c-primary); color: #fff; }
/* Gruppen */
.notes-group { display: flex; flex-direction: column; gap: var(--space-2); }
.notes-group-label { font-size: var(--text-xs); font-weight: var(--weight-semibold); color: var(--c-text-muted); text-transform: uppercase; letter-spacing: .05em; padding: var(--space-1) 0; }
/* Karten */
.notes-card { background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--radius-lg); padding: var(--space-3) var(--space-4); display: flex; flex-direction: column; gap: var(--space-2); }
.notes-card-top { display: flex; align-items: flex-start; gap: var(--space-2); }
.notes-rubrik-chip { display: inline-flex; align-items: center; gap: 4px; font-size: var(--text-xs); font-weight: var(--weight-semibold); padding: 2px var(--space-2); border-radius: 999px; flex-shrink: 0; }
.notes-parent-label { font-size: var(--text-xs); color: var(--c-text-secondary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; align-self: center; }
.notes-card-meta { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-xs); color: var(--c-text-muted); }
.notes-card-actions { display: flex; gap: var(--space-2); margin-left: auto; flex-shrink: 0; }
.notes-card-text { font-size: var(--text-sm); color: var(--c-text); line-height: 1.55; white-space: pre-wrap; margin: 0; }
.notes-micro-badges { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.notes-micro-badge { font-size: var(--text-xs); padding: 2px 6px; border-radius: var(--radius-sm); background: var(--c-surface-2); color: var(--c-text-secondary); }
.notes-action-btn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-md); border: 1px solid var(--c-border); background: var(--c-surface-2); color: var(--c-text-muted); cursor: pointer; font-size: 1rem; transition: background .15s, color .15s; }
.notes-action-btn:hover { background: var(--c-surface); color: var(--c-text); }
.notes-action-btn--danger:hover { background: #fef2f2; color: var(--c-danger); border-color: var(--c-danger); }
.notes-list { display: flex; flex-direction: column; gap: var(--space-4); }
</style>
`;
_bindEvents();
}
// ----------------------------------------------------------
// KI-Panel HTML
// ----------------------------------------------------------
function _kiPanelHtml() {
return `
<div class="notes-ki-panel" id="notes-ki-panel">
<div class="notes-ki-header" id="notes-ki-toggle">
<div class="notes-ki-header-left">
<i class="ph ph-robot"></i>
Muster-Analyse
</div>
<i class="ph ph-caret-down notes-ki-chevron ${_kiOpen ? 'notes-ki-chevron--open' : ''}" id="notes-ki-chevron"></i>
</div>
${_kiOpen ? `
<div class="notes-ki-body" id="notes-ki-body">
<button class="notes-ki-btn" id="notes-ki-analyse-btn" ${_kiLoading ? 'disabled' : ''}>
${_kiLoading ? '<i class="ph ph-spinner-gap"></i> Analysiere…' : 'Analysieren'}
</button>
${_kiError ? `<div class="notes-ki-error"><i class="ph ph-warning-circle"></i> ${_esc(_kiError)}</div>` : ''}
${_kiSuggestions ? `
<div class="notes-ki-suggestions">
<ul>
${_kiSuggestions.map(s => `<li>${_esc(s)}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
` : ''}
</div>
`;
}
// ----------------------------------------------------------
// Notiz-Karte HTML
// ----------------------------------------------------------
function _noteCard(note) {
const rb = _rubrik(note.parent_type);
const meta = note.meta_json || {};
const microBadges = [];
if (meta.erfolgsquote) microBadges.push('🐾'.repeat(meta.erfolgsquote));
if (meta.umgebung) microBadges.push({ zuhause: '🏠 Zuhause', natur: '🌿 Natur', stadt: '🌆 Stadt' }[meta.umgebung] || meta.umgebung);
if (meta.hund_stimmung) microBadges.push({ super: '😊 Super', ok: '😐 Ok', mude: '😔 Müde' }[meta.hund_stimmung] || meta.hund_stimmung);
const hasLocation = !!note.location_name;
return `
<div class="notes-card" data-id="${note.id}">
<!-- Top-Zeile: Rubrik-Chip + parent_label + Zeit + Buttons -->
<div class="notes-card-top">
<span class="notes-rubrik-chip"
style="background:${rb.color}22;color:${rb.color}">
<i class="ph ph-${rb.icon}"></i>
${_esc(rb.label)}
</span>
${note.parent_label
? `<span class="notes-parent-label" title="${_esc(note.parent_label)}">${_esc(note.parent_label)}</span>`
: ''
}
<div class="notes-card-actions">
<button class="notes-action-btn notes-edit-btn" data-id="${note.id}" title="Bearbeiten">
<i class="ph ph-pencil"></i>
</button>
<button class="notes-action-btn notes-action-btn--danger notes-delete-btn" data-id="${note.id}" title="Löschen">
<i class="ph ph-trash"></i>
</button>
</div>
</div>
<!-- Notiztext -->
<p class="notes-card-text">${_esc(_truncate(note.text))}</p>
<!-- Micro-Badges -->
${microBadges.length ? `
<div class="notes-micro-badges">
${microBadges.map(b => `<span class="notes-micro-badge">${_esc(b)}</span>`).join('')}
</div>
` : ''}
<!-- Meta: Zeit + Ort -->
<div class="notes-card-meta">
<i class="ph ph-clock"></i>
${_esc(_formatTime(note.updated_at || note.created_at))}
${hasLocation ? `<i class="ph ph-map-pin"></i> ${_esc(note.location_name)}` : ''}
</div>
</div>
`;
}
// ----------------------------------------------------------
// Event-Binding
// ----------------------------------------------------------
function _bindEvents() {
// Filter-Chips
_container.querySelectorAll('.notes-chip').forEach(btn => {
btn.addEventListener('click', () => {
_filterType = btn.dataset.type;
_reload();
});
});
// Sortierung
_container.querySelectorAll('.notes-sort-btn').forEach(btn => {
btn.addEventListener('click', () => {
_sortMode = btn.dataset.sort;
_render(); // nur neu rendern, keine API-Last
});
});
// Suche (debounced)
const searchInput = _container.querySelector('#notes-search');
if (searchInput) {
searchInput.addEventListener('input', () => {
clearTimeout(_searchTimer);
_searchTimer = setTimeout(() => {
_searchQ = searchInput.value.trim();
_reload();
}, 300);
});
}
// KI-Toggle
const kiToggle = _container.querySelector('#notes-ki-toggle');
if (kiToggle) {
kiToggle.addEventListener('click', () => {
_kiOpen = !_kiOpen;
_render();
});
}
// KI-Analyse-Button
const kiBtn = _container.querySelector('#notes-ki-analyse-btn');
if (kiBtn) {
kiBtn.addEventListener('click', async () => {
_kiLoading = true;
_kiError = null;
_kiSuggestions = null;
_render();
try {
const res = await API.notes.analyse();
if (res && Array.isArray(res.suggestions)) {
_kiSuggestions = res.suggestions;
} else if (res && res.text) {
_kiSuggestions = res.text.split('\n').filter(Boolean);
} else {
_kiSuggestions = ['Keine Vorschläge verfügbar.'];
}
} catch (err) {
_kiError = err?.message || 'KI-Analyse nicht verfügbar.';
} finally {
_kiLoading = false;
_render();
}
});
}
// Edit-Buttons
_container.querySelectorAll('.notes-edit-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const note = _notes.find(n => n.id === parseInt(btn.dataset.id, 10));
if (note) _openEditModal(note);
});
});
// Delete-Buttons
_container.querySelectorAll('.notes-delete-btn').forEach(btn => {
btn.addEventListener('click', async e => {
e.stopPropagation();
const noteId = parseInt(btn.dataset.id, 10);
if (!window.confirm('Notiz wirklich löschen?')) return;
try {
await API.notes.delete(noteId);
_notes = _notes.filter(n => n.id !== noteId);
_render();
UI.toast.success('Notiz gelöscht.');
} catch (_) {
UI.toast.error('Löschen fehlgeschlagen.');
}
});
});
}
// ----------------------------------------------------------
// Laden + Re-Render
// ----------------------------------------------------------
async function _reload() {
_container.querySelector('.notes-list')?.classList.add('loading');
try {
_notes = await _load();
} catch (_) {
_notes = [];
}
_render();
}
// ----------------------------------------------------------
// Edit-Modal (Bottom-Sheet Stil)
// ----------------------------------------------------------
function _openEditModal(note) {
const meta = note.meta_json || {};
const rb = _rubrik(note.parent_type);
const modalId = 'notes-edit-modal';
document.getElementById(modalId)?.remove();
const overlay = document.createElement('div');
overlay.id = modalId;
overlay.style.cssText = `
position:fixed;inset:0;z-index:9999;
display:flex;align-items:flex-end;justify-content:center;
background:rgba(0,0,0,0.45);
`;
overlay.innerHTML = `
<div style="background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
width:100%;max-width:480px;max-height:85vh;overflow-y:auto;
padding:var(--space-5) var(--space-4) var(--space-6);box-shadow:0 -4px 24px rgba(0,0,0,0.15)">
<!-- Griff -->
<div style="width:40px;height:4px;background:var(--c-border);border-radius:2px;margin:0 auto var(--space-4)"></div>
<!-- Kopfzeile -->
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-4)">
<span style="display:inline-flex;align-items:center;gap:4px;font-size:var(--text-xs);
font-weight:var(--weight-semibold);padding:2px var(--space-2);border-radius:999px;
background:${rb.color}22;color:${rb.color}">
<i class="ph ph-${rb.icon}"></i> ${_esc(rb.label)}
</span>
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);margin:0">
Notiz bearbeiten
</h3>
</div>
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
<!-- Freitext -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Text</label>
<textarea id="notes-edit-text" rows="5"
style="width:100%;padding:var(--space-3);border:1.5px solid var(--c-border);
border-radius:var(--radius-md);font-size:var(--text-sm);
font-family:var(--font-sans);background:var(--c-surface);
color:var(--c-text);resize:vertical;outline:none;line-height:1.5;
box-sizing:border-box">${_esc(note.text)}</textarea>
</div>
${note.parent_type === 'training_session' ? `
<!-- Bewertung -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Bewertung</label>
<div style="display:flex;gap:var(--space-2)">
${[1,2,3,4,5].map(n => `
<button type="button" class="notes-pfote" data-val="${n}"
style="font-size:1.3rem;border:1.5px solid var(--c-border);border-radius:var(--radius-md);
padding:3px 9px;cursor:pointer;
background:${(meta.erfolgsquote||0)===n?'var(--c-primary-subtle)':'var(--c-surface-2)'};
border-color:${(meta.erfolgsquote||0)===n?'var(--c-primary)':'var(--c-border)'}">🐾</button>
`).join('')}
</div>
</div>
<!-- Umgebung -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Umgebung</label>
<div style="display:flex;gap:var(--space-2)">
${[['🏠','zuhause'],['🌿','natur'],['🌆','stadt']].map(([emoji,val]) => `
<button type="button" class="notes-umgebung" data-val="${val}"
style="font-size:1.2rem;border:1.5px solid var(--c-border);border-radius:var(--radius-md);
padding:3px 10px;cursor:pointer;
background:${meta.umgebung===val?'var(--c-primary-subtle)':'var(--c-surface-2)'};
border-color:${meta.umgebung===val?'var(--c-primary)':'var(--c-border)'}">${emoji}</button>
`).join('')}
</div>
</div>
<!-- Stimmung -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Stimmung des Hundes</label>
<div style="display:flex;gap:var(--space-2)">
${[['😊','super'],['😐','ok'],['😔','mude']].map(([emoji,val]) => `
<button type="button" class="notes-stimmung" data-val="${val}"
style="font-size:1.2rem;border:1.5px solid var(--c-border);border-radius:var(--radius-md);
padding:3px 10px;cursor:pointer;
background:${meta.hund_stimmung===val?'var(--c-primary-subtle)':'var(--c-surface-2)'};
border-color:${meta.hund_stimmung===val?'var(--c-primary)':'var(--c-border)'}">${emoji}</button>
`).join('')}
</div>
</div>
` : ''}
</div>
<!-- Buttons -->
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-5)">
<button id="notes-edit-delete" type="button"
style="padding:var(--space-3) var(--space-4);border-radius:var(--radius-md);
border:1.5px solid var(--c-danger);background:none;
color:var(--c-danger);font-size:var(--text-sm);cursor:pointer">
Löschen
</button>
<button id="notes-edit-cancel" type="button"
style="flex:1;padding:var(--space-3);border-radius:var(--radius-md);
border:1.5px solid var(--c-border);background:none;
color:var(--c-text-secondary);font-size:var(--text-sm);cursor:pointer">
Abbrechen
</button>
<button id="notes-edit-save" type="button"
style="flex:2;padding:var(--space-3);border-radius:var(--radius-md);
border:none;background:var(--c-primary);
color:#fff;font-size:var(--text-sm);font-weight:var(--weight-semibold);cursor:pointer">
Speichern
</button>
</div>
</div>
`;
document.body.appendChild(overlay);
let selErfolgsquote = meta.erfolgsquote || null;
let selUmgebung = meta.umgebung || null;
let selStimmung = meta.hund_stimmung || null;
function _toggleBtn(group, val, getter, setter) {
overlay.querySelectorAll(`.notes-${group}`).forEach(b => {
const match = (group === 'pfote')
? parseInt(b.dataset.val, 10) === val
: b.dataset.val === val;
b.style.background = match ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)';
b.style.borderColor = match ? 'var(--c-primary)' : 'var(--c-border)';
});
}
overlay.querySelectorAll('.notes-pfote').forEach(btn => {
btn.addEventListener('click', () => {
const v = parseInt(btn.dataset.val, 10);
selErfolgsquote = selErfolgsquote === v ? null : v;
_toggleBtn('pfote', selErfolgsquote, null, null);
});
});
overlay.querySelectorAll('.notes-umgebung').forEach(btn => {
btn.addEventListener('click', () => {
selUmgebung = selUmgebung === btn.dataset.val ? null : btn.dataset.val;
_toggleBtn('umgebung', selUmgebung, null, null);
});
});
overlay.querySelectorAll('.notes-stimmung').forEach(btn => {
btn.addEventListener('click', () => {
selStimmung = selStimmung === btn.dataset.val ? null : btn.dataset.val;
_toggleBtn('stimmung', selStimmung, null, null);
});
});
function _close() { overlay.remove(); }
overlay.addEventListener('click', e => { if (e.target === overlay) _close(); });
overlay.querySelector('#notes-edit-cancel').addEventListener('click', _close);
// Speichern
overlay.querySelector('#notes-edit-save').addEventListener('click', async () => {
const text = overlay.querySelector('#notes-edit-text').value.trim();
if (!text) { UI.toast.warning('Notiz darf nicht leer sein.'); return; }
const saveBtn = overlay.querySelector('#notes-edit-save');
saveBtn.disabled = true;
saveBtn.textContent = 'Speichern…';
const metaObj = {};
if (selErfolgsquote) metaObj.erfolgsquote = selErfolgsquote;
if (selUmgebung) metaObj.umgebung = selUmgebung;
if (selStimmung) metaObj.hund_stimmung = selStimmung;
try {
const updated = await API.notes.update(note.id, {
text,
meta_json: Object.keys(metaObj).length > 0 ? metaObj : null,
});
const idx = _notes.findIndex(n => n.id === note.id);
if (idx >= 0) _notes[idx] = updated;
_render();
_close();
UI.toast.success('Notiz aktualisiert.');
} catch (_) {
saveBtn.disabled = false;
saveBtn.textContent = 'Speichern';
UI.toast.error('Speichern fehlgeschlagen.');
}
});
// Löschen
overlay.querySelector('#notes-edit-delete').addEventListener('click', async () => {
if (!window.confirm('Notiz wirklich löschen?')) return;
try {
await API.notes.delete(note.id);
_notes = _notes.filter(n => n.id !== note.id);
_render();
_close();
UI.toast.success('Notiz gelöscht.');
} catch (_) {
UI.toast.error('Löschen fehlgeschlagen.');
}
});
}
// ----------------------------------------------------------
// INIT / REFRESH
// ----------------------------------------------------------
async function init(container, appState) {
_container = container;
_appState = appState;
// Zustand zurücksetzen
_filterType = '';
_sortMode = 'newest';
_searchQ = '';
_kiOpen = false;
_kiLoading = false;
_kiSuggestions = null;
_kiError = null;
_notes = [];
_container.innerHTML = UI.skeleton(3);
try {
_notes = await _load();
} catch (_) {
_notes = [];
}
_render();
}
async function refresh() {
if (!_container) return;
_container.innerHTML = UI.skeleton(3);
try {
_notes = await _load();
} catch (_) {
_notes = [];
}
_render();
}
return { init, refresh };
})();

View file

@ -126,7 +126,7 @@ window.Page_onboarding = (() => {
<div style="width:36px;height:36px;border-radius:var(--radius-md);
background:var(--c-primary-subtle);flex-shrink:0;
display:flex;align-items:center;justify-content:center">
<svg style="width:18px;height:18px;color:var(--c-primary)" aria-hidden="true">
<svg style="fill:currentColor;width:18px;height:18px;color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#${icon}"></use>
</svg>
</div>
@ -167,7 +167,7 @@ window.Page_onboarding = (() => {
<div style="width:64px;height:64px;border-radius:50%;
background:var(--c-primary-subtle);margin:0 auto var(--space-4);
display:flex;align-items:center;justify-content:center">
<svg style="width:32px;height:32px;color:var(--c-primary)" aria-hidden="true">
<svg style="fill:currentColor;width:32px;height:32px;color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#dog"></use>
</svg>
</div>
@ -262,7 +262,7 @@ window.Page_onboarding = (() => {
<div style="width:80px;height:80px;border-radius:50%;
background:var(--c-success-subtle,#dcfce7);margin:0 auto var(--space-4);
display:flex;align-items:center;justify-content:center">
<svg style="width:40px;height:40px;color:var(--c-success)" aria-hidden="true">
<svg style="fill:currentColor;width:40px;height:40px;color:var(--c-success)" aria-hidden="true">
<use href="/icons/phosphor.svg#check-circle"></use>
</svg>
</div>

View file

@ -1805,6 +1805,7 @@ window.Page_routes = (() => {
${_actionBtn('rd-gpx', 'download-simple', 'GPX')}
${_actionBtn('rd-share', 'arrow-square-out', 'Teilen')}
${_actionBtn('rd-navi', 'map-pin', 'Navi')}
${_appState.user ? _actionBtn('rd-note', 'note-pencil', 'Notiz') : ''}
</div>
${ownerRow}
<button type="button" class="btn btn-primary w-full" id="rd-close">Schließen</button>
@ -1920,6 +1921,12 @@ window.Page_routes = (() => {
} catch (err) { UI.toast.error(err.message); }
});
// 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);
});
// Mini-Map
let _detailMap = null;
setTimeout(() => {
@ -2504,6 +2511,59 @@ 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" style="color:var(--c-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 };
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 };
})();

View file

@ -266,6 +266,30 @@ window.Page_settings = (() => {
</select>
</div>
<!-- KI-Notiz-Assistent -->
<div style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4)">
<svg class="ph-icon" aria-hidden="true" style="width:1.25rem;height:1.25rem"><use href="/icons/phosphor.svg#brain"></use></svg>
<div style="flex:1">
<div style="font-weight:500">KI-Notiz-Assistent</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
Erkennt Muster in deinen Notizen und macht Vorschläge
</div>
</div>
<label style="position:relative;display:inline-block;width:44px;height:24px;flex-shrink:0">
<input type="checkbox" id="toggle-notes-ki"
style="opacity:0;width:0;height:0;position:absolute"
${u.notes_ki_enabled ? 'checked' : ''}>
<span style="position:absolute;cursor:pointer;inset:0;border-radius:12px;
background:var(--c-border);transition:.2s"
id="toggle-notes-ki-track"></span>
<span id="toggle-notes-ki-thumb"
style="position:absolute;top:2px;left:${u.notes_ki_enabled ? '22px' : '2px'};
width:20px;height:20px;border-radius:50%;
background:#fff;transition:.2s;
box-shadow:0 1px 3px rgba(0,0,0,.3)"></span>
</label>
</div>
</div>
</div>
@ -635,6 +659,25 @@ window.Page_settings = (() => {
: 'Pocket-Modus deaktiviert.');
});
document.getElementById('toggle-notes-ki')?.addEventListener('change', async e => {
const enabled = e.target.checked;
const track = document.getElementById('toggle-notes-ki-track');
const thumb = document.getElementById('toggle-notes-ki-thumb');
if (track) track.style.background = enabled ? 'var(--c-primary)' : 'var(--c-border)';
if (thumb) thumb.style.left = enabled ? '22px' : '2px';
try {
await API.patch('/profile', { notes_ki_enabled: enabled ? 1 : 0 });
_appState.user.notes_ki_enabled = enabled ? 1 : 0;
UI.toast.success(enabled ? 'KI-Notiz-Assistent aktiviert.' : 'KI-Notiz-Assistent deaktiviert.');
} catch (err) {
UI.toast.error(err?.message || 'Einstellung konnte nicht gespeichert werden.');
// Revert UI
e.target.checked = !enabled;
if (track) track.style.background = !enabled ? 'var(--c-primary)' : 'var(--c-border)';
if (thumb) thumb.style.left = !enabled ? '22px' : '2px';
}
});
_loadReferral();
}

View file

@ -136,6 +136,12 @@ window.Page_sitting = (() => {
<div class="sitting-card-side">
<div class="sitting-price">${s.preis_pro_tag > 0 ? s.preis_pro_tag.toFixed(0) + ' €/Tag' : 'Preis anfragen'}</div>
<div class="sitting-dogs">max. ${s.max_hunde} Hund${s.max_hunde !== 1 ? 'e' : ''}</div>
${_state.user ? `<button class="btn-icon sit-note-btn"
data-sit-note-id="${s.id}"
data-sit-note-label="${UI.escHtml(s.sitter_name + ' ' + (s.datum || ''))}"
title="Notiz" style="color:var(--c-text-muted);margin-top:var(--space-1)"
onclick="event.stopPropagation()">
${UI.icon('note-pencil')}</button>` : ''}
</div>
</div>
`;
@ -704,6 +710,19 @@ window.Page_sitting = (() => {
return;
}
// Notiz-Button auf Sitter-Karte
const noteBtn = e.target.closest('.sit-note-btn');
if (noteBtn) {
e.stopPropagation();
_openNoteModal(
'sitting',
parseInt(noteBtn.dataset.sitNoteId),
noteBtn.dataset.sitNoteLabel,
null
);
return;
}
// Sitter-Karte
const sitterCard = e.target.closest('[data-sit-id]');
if (sitterCard && !e.target.closest('button')) {
@ -741,6 +760,59 @@ window.Page_sitting = (() => {
} catch (e) { UI.toast(e.message, 'error'); }
}
// ----------------------------------------------------------
// 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" style="color:var(--c-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 };
})();

View file

@ -895,6 +895,7 @@ window.Page_uebungen = (() => {
_bindAccordions();
_bindStatusButtons();
_bindLogButtons();
_bindNotizButtons();
if (_activeTab === 'ki-trainer') _loadKiTrainerFeedback();
}
@ -965,6 +966,19 @@ window.Page_uebungen = (() => {
Einheit
</button>
${_sessionStatsChip(_activeTab, u.name)}
<button class="ueb-notiz-btn"
data-tab="${_esc(_activeTab)}"
data-name="${_esc(u.name)}"
title="Notiz hinzufügen"
style="background:none;border:1px solid var(--c-border);cursor:pointer;
padding:3px 7px;border-radius:var(--radius-sm);
display:flex;align-items:center;gap:3px;
font-size:var(--text-xs);color:var(--c-text-secondary)">
<svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0" aria-hidden="true">
<use href="/icons/phosphor.svg#note-pencil"></use>
</svg>
Notiz
</button>
<button class="ueb-status-btn"
data-tab="${_esc(_activeTab)}"
data-name="${_esc(u.name)}"
@ -1006,7 +1020,7 @@ window.Page_uebungen = (() => {
background:#78350f22;border:1px solid #d9770644;border-radius:var(--radius-sm);
font-size:var(--text-xs);color:var(--c-text);line-height:1.4;
display:flex;align-items:flex-start;gap:var(--space-2)">
<svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0;margin-top:1px;color:#f59e0b" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
<svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0;margin-top:1px;color:var(--c-warning)" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
<span>${_esc(u.hinweis)}</span>
</div>
` : ''}
@ -1039,7 +1053,7 @@ window.Page_uebungen = (() => {
${u.fehler.length ? `
<p style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
color:var(--c-text-secondary);margin-bottom:var(--space-2);text-transform:uppercase;letter-spacing:0.05em">
<svg class="ph-icon" style="width:12px;height:12px;color:#f59e0b" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
<svg class="ph-icon" style="width:12px;height:12px;color:var(--c-warning)" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
Häufige Fehler
</p>
<ul style="margin:0 0 var(--space-4);padding-left:var(--space-5);display:flex;flex-direction:column;gap:var(--space-2)">
@ -1100,6 +1114,252 @@ window.Page_uebungen = (() => {
});
}
function _bindNotizButtons() {
_container.querySelectorAll('.ueb-notiz-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const exerciseId = `${btn.dataset.tab}_${btn.dataset.name.replace(/[\s/]+/g, '_')}`;
_openNotizModal(exerciseId, btn.dataset.name, btn);
});
});
}
async function _openNotizModal(exerciseId, exerciseName, triggerBtn) {
const modalId = 'ueb-notiz-modal';
document.getElementById(modalId)?.remove();
// Lade bestehende Notiz
let existingNote = null;
if (_appState?.user) {
try {
const notes = await API.notes.get('training_session', exerciseId.length > 0 ? exerciseId : 0);
if (notes && notes.length > 0) existingNote = notes[0];
} catch (_) {}
}
const overlay = document.createElement('div');
overlay.id = modalId;
overlay.style.cssText = `
position:fixed;inset:0;z-index:9999;
display:flex;align-items:flex-end;justify-content:center;
background:rgba(0,0,0,0.45);
`;
const noteText = existingNote?.text || '';
const meta = existingNote?.meta_json || {};
const currentErfolgsquote = meta.erfolgsquote || null;
const currentUmgebung = meta.umgebung || null;
const currentStimmung = meta.hund_stimmung || null;
overlay.innerHTML = `
<div style="background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
width:100%;max-width:480px;max-height:85vh;overflow-y:auto;
padding:var(--space-5) var(--space-4) var(--space-6);box-shadow:0 -4px 24px rgba(0,0,0,0.15)">
<div style="width:40px;height:4px;background:var(--c-border);border-radius:2px;margin:0 auto var(--space-4)"></div>
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);
margin:0 0 var(--space-4);text-align:center">
Notiz: ${_esc(exerciseName)}
</h3>
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
<!-- Freitext -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Notiz</label>
<textarea id="ueb-notiz-text" rows="3"
placeholder="Was ist dir aufgefallen? Tipps für nächstes Mal…"
style="width:100%;padding:var(--space-3);border:1.5px solid var(--c-border);
border-radius:var(--radius-md);font-size:var(--text-sm);
font-family:var(--font-sans);background:var(--c-surface);
color:var(--c-text);resize:vertical;outline:none;
line-height:1.5">${_esc(noteText)}</textarea>
</div>
<!-- Erfolgsquote -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Bewertung (optional)</label>
<div style="display:flex;gap:var(--space-2)">
${[1,2,3,4,5].map(n => `
<button type="button" class="ueb-notiz-pfote" data-val="${n}"
style="font-size:1.4rem;border:1.5px solid var(--c-border);
border-radius:var(--radius-md);padding:4px 10px;cursor:pointer;
background:${currentErfolgsquote === n ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)'};
border-color:${currentErfolgsquote === n ? 'var(--c-primary)' : 'var(--c-border)'};
transition:all 0.15s">🐾</button>
`).join('')}
</div>
</div>
<!-- Umgebung -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Umgebung (optional)</label>
<div style="display:flex;gap:var(--space-2)">
${[['🏠','zuhause'],['🌿','natur'],['🌆','stadt']].map(([emoji, val]) => `
<button type="button" class="ueb-notiz-umgebung" data-val="${val}"
style="font-size:1.2rem;border:1.5px solid var(--c-border);
border-radius:var(--radius-md);padding:4px 12px;cursor:pointer;
background:${currentUmgebung === val ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)'};
border-color:${currentUmgebung === val ? 'var(--c-primary)' : 'var(--c-border)'};
transition:all 0.15s">${emoji}</button>
`).join('')}
</div>
</div>
<!-- Hund-Stimmung -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Stimmung des Hundes (optional)</label>
<div style="display:flex;gap:var(--space-2)">
${[['😊','super'],['😐','ok'],['😔','mude']].map(([emoji, val]) => `
<button type="button" class="ueb-notiz-stimmung" data-val="${val}"
style="font-size:1.2rem;border:1.5px solid var(--c-border);
border-radius:var(--radius-md);padding:4px 12px;cursor:pointer;
background:${currentStimmung === val ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)'};
border-color:${currentStimmung === val ? 'var(--c-primary)' : 'var(--c-border)'};
transition:all 0.15s">${emoji}</button>
`).join('')}
</div>
</div>
</div>
<!-- Buttons -->
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-5)">
${existingNote ? `
<button id="ueb-notiz-delete" type="button"
style="padding:var(--space-3) var(--space-4);border-radius:var(--radius-md);
border:1.5px solid var(--c-danger);background:none;
color:var(--c-danger);font-size:var(--text-sm);cursor:pointer">
Löschen
</button>
` : ''}
<button id="ueb-notiz-cancel" type="button"
style="flex:1;padding:var(--space-3);border-radius:var(--radius-md);
border:1.5px solid var(--c-border);background:none;
color:var(--c-text-secondary);font-size:var(--text-sm);cursor:pointer">
Abbrechen
</button>
<button id="ueb-notiz-save" type="button"
style="flex:2;padding:var(--space-3);border-radius:var(--radius-md);
border:none;background:var(--c-primary);
color:#fff;font-size:var(--text-sm);font-weight:var(--weight-semibold);cursor:pointer">
${existingNote ? 'Aktualisieren' : 'Speichern'}
</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// State
let selectedErfolgsquote = currentErfolgsquote;
let selectedUmgebung = currentUmgebung;
let selectedStimmung = currentStimmung;
// Pfoten-Buttons
overlay.querySelectorAll('.ueb-notiz-pfote').forEach(btn => {
btn.addEventListener('click', () => {
const val = parseInt(btn.dataset.val, 10);
selectedErfolgsquote = selectedErfolgsquote === val ? null : val;
overlay.querySelectorAll('.ueb-notiz-pfote').forEach(b => {
const active = parseInt(b.dataset.val, 10) === selectedErfolgsquote;
b.style.background = active ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)';
b.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)';
});
});
});
// Umgebung-Buttons
overlay.querySelectorAll('.ueb-notiz-umgebung').forEach(btn => {
btn.addEventListener('click', () => {
selectedUmgebung = selectedUmgebung === btn.dataset.val ? null : btn.dataset.val;
overlay.querySelectorAll('.ueb-notiz-umgebung').forEach(b => {
const active = b.dataset.val === selectedUmgebung;
b.style.background = active ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)';
b.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)';
});
});
});
// Stimmung-Buttons
overlay.querySelectorAll('.ueb-notiz-stimmung').forEach(btn => {
btn.addEventListener('click', () => {
selectedStimmung = selectedStimmung === btn.dataset.val ? null : btn.dataset.val;
overlay.querySelectorAll('.ueb-notiz-stimmung').forEach(b => {
const active = b.dataset.val === selectedStimmung;
b.style.background = active ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)';
b.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)';
});
});
});
function _closeNotizModal() {
overlay.remove();
}
overlay.addEventListener('click', e => { if (e.target === overlay) _closeNotizModal(); });
overlay.querySelector('#ueb-notiz-cancel').addEventListener('click', _closeNotizModal);
// Speichern
overlay.querySelector('#ueb-notiz-save').addEventListener('click', async () => {
const text = overlay.querySelector('#ueb-notiz-text').value.trim();
if (!text) { UI.toast.warning('Bitte gib eine Notiz ein.'); return; }
const saveBtn = overlay.querySelector('#ueb-notiz-save');
saveBtn.disabled = true;
saveBtn.textContent = 'Speichern…';
const meta = {};
if (selectedErfolgsquote) meta.erfolgsquote = selectedErfolgsquote;
if (selectedUmgebung) meta.umgebung = selectedUmgebung;
if (selectedStimmung) meta.hund_stimmung = selectedStimmung;
const payload = {
text,
meta_json: Object.keys(meta).length > 0 ? meta : null,
};
try {
if (existingNote) {
await API.notes.update(existingNote.id, payload);
} else {
await API.notes.create('training_session', exerciseId, payload);
}
_closeNotizModal();
UI.toast.success('Notiz gespeichert.');
// Notiz-Button leicht hervorheben
if (triggerBtn) {
triggerBtn.style.borderColor = 'var(--c-primary)';
triggerBtn.style.color = 'var(--c-primary)';
}
} catch (err) {
saveBtn.disabled = false;
saveBtn.textContent = existingNote ? 'Aktualisieren' : 'Speichern';
UI.toast.error('Speichern fehlgeschlagen.');
}
});
// Löschen
overlay.querySelector('#ueb-notiz-delete')?.addEventListener('click', async () => {
if (!existingNote) return;
try {
await API.notes.delete(existingNote.id);
_closeNotizModal();
UI.toast.success('Notiz gelöscht.');
if (triggerBtn) {
triggerBtn.style.borderColor = '';
triggerBtn.style.color = '';
}
} catch (_) {
UI.toast.error('Löschen fehlgeschlagen.');
}
});
}
function _openLogModal(tab, exerciseName, initialReps) {
// Build the modal HTML
const modalId = 'ueb-log-modal';

View file

@ -192,6 +192,18 @@ window.Page_walks = (() => {
el.querySelectorAll('.walks-card').forEach(card => {
card.addEventListener('click', () => _openDetail(parseInt(card.dataset.id)));
});
el.querySelectorAll('.wk-note-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
_openNoteModal(
'walk',
parseInt(btn.dataset.wkNoteId),
btn.dataset.wkNoteLabel,
btn.dataset.wkNoteOrt || null
);
});
});
}
function _walkCardHTML(w) {
@ -217,7 +229,16 @@ window.Page_walks = (() => {
${isOwn ? '<span class="walks-badge walks-badge--own">Mein Treffen</span>' : ''}
</div>
</div>
<div class="walks-card-arrow"></div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:var(--space-1)">
<div class="walks-card-arrow"></div>
${_appState.user ? `<button class="btn-icon wk-note-btn"
data-wk-note-id="${w.id}"
data-wk-note-label="${UI.escape(w.titel + ' ' + w.datum)}"
data-wk-note-ort="${UI.escape(w.ort_name || '')}"
title="Notiz" style="color:var(--c-text-muted);font-size:var(--text-xs)"
onclick="event.stopPropagation()">
${UI.icon('note-pencil')}</button>` : ''}
</div>
</div>`;
}
@ -964,6 +985,59 @@ 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" style="color:var(--c-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.'); }
});
}
return { init, refresh, onDogChange, openNew, openDetail: _openDetail };
})();

View file

@ -53,7 +53,7 @@ window.Page_welcome = (() => {
style="width:36px;height:36px;border-radius:var(--radius-md);
background:var(--c-primary);flex-shrink:0;
display:flex;align-items:center;justify-content:center">
<svg style="width:20px;height:20px;color:#fff" aria-hidden="true">
<svg style="fill:currentColor;width:20px;height:20px;color:#fff" aria-hidden="true">
<use href="/icons/phosphor.svg#list"></use>
</svg>
</div>
@ -237,7 +237,7 @@ window.Page_welcome = (() => {
<div style="width:34px;height:34px;border-radius:var(--radius-md);
background:var(--c-primary-subtle);flex-shrink:0;
display:flex;align-items:center;justify-content:center">
<svg style="width:18px;height:18px;color:var(--c-primary)" aria-hidden="true">
<svg style="fill:currentColor;width:18px;height:18px;color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#${icon}"></use>
</svg>
</div>

View file

@ -3,15 +3,15 @@
Offline-Cache + Push Notifications + Tile-Cache
============================================================ */
const CACHE_VERSION = 'by-v370';
const CACHE_VERSION = 'by-v405';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
// index.html wird NICHT pre-gecacht (immer Network-First)
const STATIC_ASSETS = [
'/css/design-system.css',
'/css/layout.css',
'/css/components.css',
'/css/design-system.css?v=382',
'/css/layout.css?v=382',
'/css/components.css?v=382',
'/icons/phosphor.svg',
'/js/api.js',
'/js/ui.js',
@ -82,8 +82,8 @@ self.addEventListener('fetch', event => {
return;
}
// Seiten-Module (/js/pages/…): immer Network-First (versioniert über ?v=, kein alter Cache-Treffer)
if (url.pathname.startsWith('/js/pages/')) {
// CSS + Seiten-Module: immer Network-First — damit iOS nie veraltete CSS cached
if (url.pathname.startsWith('/css/') || url.pathname.startsWith('/js/pages/')) {
event.respondWith(
fetch(event.request)
.then(response => {