v1.7.4: Suche in Listenansicht (Bemerkungen, Objekte, BEOs)
Suchfeld in der Toolbar der Listenansicht: Suche über alle Einträge der Kuppel in Bemerkungen, Objekte und BEOs. Monatsauswahl, Suchfeld und Drucken-Button in einer Zeile; Monatsauswahl wird bei aktiver Suche unsichtbar aber platzhaltend ausgeblendet. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+165
@@ -0,0 +1,165 @@
|
||||
# Bedienungsanleitung – Logbuch Sternwarte Welzheim
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
|
||||
1. [Anmelden](#1-anmelden)
|
||||
2. [Grundaufbau der App](#2-grundaufbau-der-app)
|
||||
3. [Eintrag erfassen (Tab „Eingabe")](#3-eintrag-erfassen-tab-eingabe)
|
||||
4. [Einträge einsehen und verwalten (Tab „Liste")](#4-einträge-einsehen-und-verwalten-tab-liste)
|
||||
5. [Jahresstatistik (Tab „Statistik")](#5-jahresstatistik-tab-statistik)
|
||||
6. [Drucken](#6-drucken)
|
||||
7. [Administration (nur Admins)](#7-administration-nur-admins)
|
||||
|
||||
---
|
||||
|
||||
## 1. Anmelden
|
||||
|
||||
Die App ist passwortgeschützt. Beim ersten Aufruf erscheint die Anmeldeseite.
|
||||
|
||||
- **Kürzel**: das persönliche BEO-Kürzel (z. B. `RXF`)
|
||||
- **Passwort**: individuell gesetztes Passwort
|
||||
|
||||
Wurde das Passwort noch nicht geändert (Anzeige „Standard"), muss nach dem ersten Login sofort ein neues Passwort vergeben werden. Das Standard-Passwort lautet `welzheim`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Grundaufbau der App
|
||||
|
||||
### Kuppel-Auswahl
|
||||
|
||||
Oben befinden sich vier Reiter für die vier Kuppeln:
|
||||
|
||||
| Reiter | Bedeutung |
|
||||
|--------|-----------|
|
||||
| West | West-Kuppel |
|
||||
| Ost | Ost-Kuppel |
|
||||
| Süd | Süd-Kuppel |
|
||||
| Pluto | Pluto-Kuppel |
|
||||
|
||||
Alle Einträge, Listen und Statistiken beziehen sich immer auf die gerade gewählte Kuppel.
|
||||
|
||||
### Funktions-Tabs
|
||||
|
||||
Unterhalb der Kuppelauswahl gibt es drei Tabs:
|
||||
|
||||
| Tab | Funktion |
|
||||
|-----|----------|
|
||||
| **Eingabe** | Neuen Eintrag anlegen oder bestehenden bearbeiten |
|
||||
| **Liste** | Alle Einträge monatsweise ansehen, bearbeiten oder löschen |
|
||||
| **Statistik** | Jahresübersicht Besucher und Führungen |
|
||||
|
||||
---
|
||||
|
||||
## 3. Eintrag erfassen (Tab „Eingabe")
|
||||
|
||||
### Pflichtfelder
|
||||
|
||||
**Art der Führung** – Auswahl aus dem Dropdown:
|
||||
|
||||
| Kürzel | Bedeutung |
|
||||
|--------|-----------|
|
||||
| regulär | Reguläre öffentliche Führung |
|
||||
| sonder | Sonderführung (für Gruppen, Schulen etc.) |
|
||||
| sonnen | Sonnenführung |
|
||||
| privat | Privatführung |
|
||||
| BEOS | BEO-Sitzung (keine Besucher/Objekte) |
|
||||
| TD | Treff/Diskussion (keine Besucher/Objekte) |
|
||||
| Beobachtung | Reine Beobachtung ohne Führung |
|
||||
| ToT | Teleskop ohne Termin |
|
||||
| Sonstiges | Sonstige Veranstaltung |
|
||||
|
||||
**Datum** – Datum der Veranstaltung (Standardwert: heute).
|
||||
|
||||
**Startzeit / Endzeit** – Uhrzeit von Beginn und Ende. Die Startzeit wird beim Laden automatisch auf die aktuelle Uhrzeit gesetzt; die Endzeit ist zunächst leer und muss eingetragen werden.
|
||||
|
||||
**Besucher** – Anzahl der Besucher (nicht bei BEOS und TD).
|
||||
|
||||
### Optionale Felder
|
||||
|
||||
**Name / Gruppe** – erscheint nur bei Sonderführung; Name der Gruppe oder Person.
|
||||
|
||||
**BEOs** – beteiligte Beobachter. Der eigene Name ist automatisch vorausgewählt. Weitere BEOs können über das Suchfeld hinzugefügt werden; ein Klick auf × entfernt sie wieder.
|
||||
|
||||
**Beobachtete Objekte** – nicht sichtbar bei BEOS und TD; bei Sonnenführungen fest auf „Sonne" gesetzt. Für alle anderen Arten:
|
||||
- Bekannte Objekte durch Eintippen suchen und aus dem Dropdown auswählen.
|
||||
- Noch unbekannte Objekte einfach eintippen – am Ende der Dropdown-Liste erscheint dann **+ „[Name]" hinzufügen**. Ein Klick (oder Enter bei leerem Suchergebnis) legt das Objekt neu an.
|
||||
- Ausgewählte Objekte erscheinen als grüne Chips; × entfernt sie.
|
||||
|
||||
**Bemerkungen** – freier Text, max. 500 Zeichen.
|
||||
|
||||
**Wetterdaten** – Temperatur (°C), Luftfeuchtigkeit (%) und Luftdruck (hPa) werden automatisch vom lokalen Wetterdienst vorausgefüllt und können manuell korrigiert werden.
|
||||
|
||||
### Eintrag speichern
|
||||
|
||||
Schaltfläche **Eintrag speichern** unten im Formular. Eine grüne Meldung bestätigt die Speicherung; das Formular wird zurückgesetzt.
|
||||
|
||||
> Auf Desktop-Geräten erscheinen unterhalb des Formulars die letzten 5 Einträge der aktuellen Kuppel als kompakte Vorschau.
|
||||
|
||||
### Eintrag bearbeiten
|
||||
|
||||
Im Tab „Liste" das Stift-Symbol (✎) anklicken. Die App springt zum Tab „Eingabe" und zeigt einen gelben Hinweis „Eintrag bearbeiten (ID …)". Nach der Änderung **Änderungen speichern** klicken oder mit **Abbrechen** verwerfen.
|
||||
|
||||
---
|
||||
|
||||
## 4. Einträge einsehen und verwalten (Tab „Liste")
|
||||
|
||||
### Monatsnavigation
|
||||
|
||||
Mit den Pfeiltasten ← → den Monat wechseln oder direkt im Monatsfeld eingeben. **Aktueller Monat** springt zurück auf den laufenden Monat.
|
||||
|
||||
### Tabelleninhalt
|
||||
|
||||
Die Tabelle zeigt pro Eintrag: Datum, Uhrzeit (Beginn–Ende), Art der Führung, Besucher, beteiligte BEOs, beobachtete Objekte, Bemerkungen und Wetterdaten. Der Ersteller des Eintrags ist in der BEO-Spalte **fettgedruckt** und steht an erster Stelle.
|
||||
|
||||
### Eintrag bearbeiten
|
||||
|
||||
Stift-Symbol ✎ rechts in der Zeile.
|
||||
|
||||
### Eintrag löschen
|
||||
|
||||
× rechts in der Zeile – es erscheint ein Bestätigungsdialog. Das Löschen ist **unwiderruflich**.
|
||||
|
||||
### Seitennavigation
|
||||
|
||||
Bei mehr als 15 Einträgen im Monat erscheinen Vor/Zurück-Schaltflächen am unteren Rand.
|
||||
|
||||
---
|
||||
|
||||
## 5. Jahresstatistik (Tab „Statistik")
|
||||
|
||||
Zeigt eine Monatstabelle mit Anzahl der Führungen und Besuchern, aufgeschlüsselt nach Art der Führung.
|
||||
|
||||
- **Jahr** oben links änderbar (Eingabefeld).
|
||||
- Darunter vier Kennzahlen-Kacheln:
|
||||
- Kumulierte Besucher des Jahres für die gewählte Kuppel
|
||||
- Führungstage des Jahres für die gewählte Kuppel
|
||||
- Kumulierte Besucher für die gesamte Sternwarte (alle Kuppeln)
|
||||
- Führungstage für die gesamte Sternwarte
|
||||
|
||||
---
|
||||
|
||||
## 6. Drucken
|
||||
|
||||
Im Tab **Liste**: Schaltfläche **🖨 Drucken** oben rechts in der Liste.
|
||||
|
||||
- Es werden **alle Einträge des aktuell gewählten Monats** geladen (nicht nur die angezeigte Seite).
|
||||
- Die Reihenfolge ist beim Ausdruck **chronologisch** (ältester Eintrag zuerst).
|
||||
- Navigations- und Aktionselemente werden ausgeblendet; oben erscheint ein Kopfzeile mit Kuppelname und Druckdatum.
|
||||
- Seitenformat: A4 Hochformat, Rand 1,5 cm.
|
||||
|
||||
Im Tab **Statistik**: ebenfalls eine **🖨 Drucken**-Schaltfläche für die Jahresstatistik.
|
||||
|
||||
---
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 7. Administration (nur Admins)
|
||||
|
||||
Erreichbar über die Schaltfläche **Admin** oben rechts (nur für Benutzer mit Admin-Rolle sichtbar).
|
||||
|
||||
### Benutzerverwaltung
|
||||
|
||||
Die Tabelle zeigt alle BEOs mit Kürzel, Name, Vorname, Rolle und Passwortstatus.
|
||||
|
||||
**Passwort zurücksetzen**: Schaltfläche „Zurücksetzen" neben dem jeweiligen Benutzer. Das Passwort wird auf NULL gesetzt; beim nächsten Login muss der Benutzer das Standard-Passwort `welzheim` verwenden und wird anschließend aufgefordert, ein neues Passwort zu vergeben.
|
||||
@@ -33,6 +33,37 @@ export async function GET(request: NextRequest) {
|
||||
const month = searchParams.get('month') || '';
|
||||
const order = searchParams.get('order') === 'asc' ? 'ASC' : 'DESC';
|
||||
|
||||
const search = (searchParams.get('search') || '').trim();
|
||||
|
||||
if (search) {
|
||||
const pattern = '%' + search + '%';
|
||||
const searchParams2 = [kuppel, pattern, pattern, pattern];
|
||||
const countSQL =
|
||||
'SELECT COUNT(*) AS total FROM (' +
|
||||
'SELECT l.ID FROM logbuch l' +
|
||||
' LEFT JOIN logbuch_beos lb ON lb.LogbuchID = l.ID' +
|
||||
' LEFT JOIN (SELECT id, `kürzel` AS kuerzel FROM beos) bk ON bk.id = lb.BeoID' +
|
||||
' LEFT JOIN logbuch_objekte lo ON lo.LogbuchID = l.ID' +
|
||||
' LEFT JOIN objekte o ON o.ID = lo.ObjektID' +
|
||||
' WHERE l.Kuppel = ?' +
|
||||
' GROUP BY l.ID' +
|
||||
" HAVING (MAX(l.Bemerkungen) LIKE ? OR GROUP_CONCAT(DISTINCT bk.kuerzel ORDER BY bk.kuerzel SEPARATOR ', ') LIKE ? OR GROUP_CONCAT(DISTINCT o.Name ORDER BY o.Name SEPARATOR ', ') LIKE ?)" +
|
||||
') AS sub';
|
||||
const listSQL = LIST_SQL +
|
||||
' HAVING (MAX(l.Bemerkungen) LIKE ? OR BEOs LIKE ? OR Objekte LIKE ?)' +
|
||||
` ORDER BY l.Beginn DESC LIMIT ${limit} OFFSET ${offset}`;
|
||||
try {
|
||||
const [countRows, entries] = await Promise.all([
|
||||
query(countSQL, searchParams2) as Promise<{ total: number }[]>,
|
||||
query(listSQL, searchParams2),
|
||||
]);
|
||||
return NextResponse.json({ entries, total: (countRows as unknown as { total: number }[])[0]?.total ?? 0 });
|
||||
} catch (error) {
|
||||
console.error('GET /api/logbuch (search):', error);
|
||||
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
let listWhere = 'WHERE l.Kuppel = ?';
|
||||
let countWhere = 'WHERE Kuppel = ?';
|
||||
let params: (string | number | null)[] = [kuppel];
|
||||
|
||||
+46
-26
@@ -59,20 +59,27 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
|
||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [printEntries, setPrintEntries] = useState<LogbuchEintrag[] | null>(null);
|
||||
const [search, setSearch] = useState('');
|
||||
const [activeSearch, setActiveSearch] = useState('');
|
||||
const printPending = useRef(false);
|
||||
|
||||
useEffect(() => { setPage(0); }, [kuppel, refreshKey, month]);
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setActiveSearch(search.trim()), 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [search]);
|
||||
|
||||
useEffect(() => { setPage(0); }, [kuppel, refreshKey, month, activeSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
const offset = page * limit;
|
||||
const url = `/api/logbuch?kuppel=${encodeURIComponent(kuppel)}&limit=${limit}&offset=${offset}` +
|
||||
(month ? `&month=${encodeURIComponent(month)}` : '');
|
||||
(activeSearch ? `&search=${encodeURIComponent(activeSearch)}` : (month ? `&month=${encodeURIComponent(month)}` : ''));
|
||||
fetch(url)
|
||||
.then((r) => { if (!r.ok) throw new Error(); return r.json(); })
|
||||
.then((data) => { setEntries(data.entries); setTotal(data.total); setLoading(false); })
|
||||
.catch(() => { setError('Fehler beim Laden.'); setLoading(false); });
|
||||
}, [kuppel, refreshKey, limit, page, month]);
|
||||
}, [kuppel, refreshKey, limit, page, month, activeSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
function onAfterPrint() { setPrintEntries(null); }
|
||||
@@ -108,8 +115,12 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
|
||||
}
|
||||
}
|
||||
|
||||
const monthNav = !compact && (
|
||||
const toolbar = !compact && (
|
||||
<div className="flex items-center gap-2 mb-3 print:hidden">
|
||||
<div
|
||||
className="flex items-center gap-1 shrink-0"
|
||||
style={{ visibility: activeSearch ? 'hidden' : 'visible' }}
|
||||
>
|
||||
<button
|
||||
onClick={() => setMonth((m) => prevMonth(m))}
|
||||
className="px-2 py-1 text-sm rounded-lg bg-gray-200 hover:bg-gray-300"
|
||||
@@ -129,12 +140,33 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
|
||||
{month !== currentMonth() && (
|
||||
<button
|
||||
onClick={() => setMonth(currentMonth())}
|
||||
className="text-sm text-blue-600 hover:underline"
|
||||
>
|
||||
Aktueller Monat
|
||||
</button>
|
||||
className="text-sm text-blue-600 hover:underline ml-1"
|
||||
>Aktueller Monat</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative flex-1 min-w-0 mx-3">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Suche in Bemerkungen, Objekte, BEOs…"
|
||||
className="w-full px-3 py-1.5 pr-8 border border-gray-300 rounded-lg text-sm focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
{search ? (
|
||||
<button
|
||||
onClick={() => setSearch('')}
|
||||
aria-label="Suche löschen"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-700 text-sm leading-none"
|
||||
>✕</button>
|
||||
) : (
|
||||
<span className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none text-sm">🔍</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="text-sm px-3 py-1.5 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg shrink-0"
|
||||
>🖨 Drucken</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const printHeader = !compact && (
|
||||
@@ -150,27 +182,15 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
|
||||
? 'px-1.5 py-1 border border-gray-300 text-xs font-semibold'
|
||||
: 'px-3 py-2 border border-gray-300';
|
||||
|
||||
if (loading) return <>{monthNav}<div className="text-gray-500 text-sm py-4">Lade Einträge...</div></>;
|
||||
if (error) return <>{monthNav}<div className="text-red-600 text-sm py-4">{error}</div></>;
|
||||
|
||||
const displayEntries = printEntries ?? entries;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!compact && (
|
||||
<div className="flex justify-between items-center mb-2 print:hidden">
|
||||
<span className="text-sm font-semibold text-gray-600">Einträge {kuppel}-Kuppel</span>
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="text-sm px-3 py-1.5 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg"
|
||||
>
|
||||
🖨 Drucken
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{monthNav}
|
||||
{toolbar}
|
||||
{printHeader}
|
||||
<div className="overflow-x-auto">
|
||||
{loading && <div className="text-gray-500 text-sm py-4">Lade Einträge...</div>}
|
||||
{error && <div className="text-red-600 text-sm py-4">{error}</div>}
|
||||
{!loading && !error && <div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse" style={{ fontSize: compact ? '0.75rem' : '0.875rem' }}>
|
||||
<thead>
|
||||
<tr className="bg-gray-100 text-left">
|
||||
@@ -196,7 +216,7 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
|
||||
{displayEntries.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={compact ? 7 : 10} className="px-3 py-4 text-gray-500 text-sm text-center">
|
||||
Keine Einträge für {monthLabel(month)}.
|
||||
{activeSearch ? `Keine Einträge für „${activeSearch}" gefunden.` : `Keine Einträge für ${monthLabel(month)}.`}
|
||||
</td>
|
||||
</tr>
|
||||
) : displayEntries.map((e) => (
|
||||
@@ -257,7 +277,7 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{!compact && total > limit && (
|
||||
<div className="flex items-center justify-center gap-3 mt-3 print:hidden">
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "logbuch",
|
||||
"version": "1.7.3",
|
||||
"version": "1.7.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "logbuch",
|
||||
"version": "1.7.3",
|
||||
"version": "1.7.4",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"jose": "^6.2.2",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "logbuch",
|
||||
"version": "1.7.3",
|
||||
"version": "1.7.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Reference in New Issue
Block a user