Initial implementation: Logbuch Sternwarte Welzheim
Vollständige Next.js 16 Webanwendung als Logbuch für die Sternwarte Welzheim. 4 Kuppeln (West/Ost/Süd/Pluto), BEO-basierte Authentifizierung mit erzwungenem Passwort-Wechsel beim Erstlogin, MySQL-Backend, Docker-Deployment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
121
components/LogbuchList.tsx
Normal file
121
components/LogbuchList.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { Kuppel, LogbuchEintrag } from '@/types/logbuch';
|
||||
|
||||
interface Props {
|
||||
kuppel: Kuppel;
|
||||
refreshKey: number;
|
||||
onEdit: (entry: LogbuchEintrag) => void;
|
||||
}
|
||||
|
||||
function formatDateTime(dt: string): string {
|
||||
if (!dt) return '';
|
||||
const d = new Date(dt);
|
||||
if (isNaN(d.getTime())) return dt;
|
||||
return d.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
export default function LogbuchList({ kuppel, refreshKey, onEdit }: Props) {
|
||||
const [entries, setEntries] = useState<LogbuchEintrag[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetch(`/api/logbuch?kuppel=${encodeURIComponent(kuppel)}&limit=20`)
|
||||
.then((r) => { if (!r.ok) throw new Error(); return r.json(); })
|
||||
.then((data) => { setEntries(data); setLoading(false); })
|
||||
.catch(() => { setError('Fehler beim Laden.'); setLoading(false); });
|
||||
}, [kuppel, refreshKey]);
|
||||
|
||||
async function confirmDelete(id: number) {
|
||||
try {
|
||||
const res = await fetch(`/api/logbuch/${id}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error();
|
||||
setEntries((prev) => prev.filter((e) => e.ID !== id));
|
||||
} catch {
|
||||
setError('Fehler beim Löschen.');
|
||||
} finally {
|
||||
setDeleteId(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div className="text-gray-500 text-sm py-4">Lade Einträge...</div>;
|
||||
if (error) return <div className="text-red-600 text-sm py-4">{error}</div>;
|
||||
if (entries.length === 0) return <div className="text-gray-500 text-sm py-4">Keine Einträge vorhanden.</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-100 text-left">
|
||||
<th className="px-3 py-2 border border-gray-300 whitespace-nowrap">Beginn</th>
|
||||
<th className="px-3 py-2 border border-gray-300 whitespace-nowrap">Ende</th>
|
||||
<th className="px-3 py-2 border border-gray-300">Art</th>
|
||||
<th className="px-3 py-2 border border-gray-300 text-center">Besucher</th>
|
||||
<th className="px-3 py-2 border border-gray-300">BEOs</th>
|
||||
<th className="px-3 py-2 border border-gray-300">Objekte</th>
|
||||
<th className="px-3 py-2 border border-gray-300">Bemerkungen</th>
|
||||
<th className="px-3 py-2 border border-gray-300 text-center">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((e) => (
|
||||
<tr key={e.ID} className="hover:bg-gray-50">
|
||||
<td className="px-3 py-2 border border-gray-200 whitespace-nowrap">{formatDateTime(e.Beginn)}</td>
|
||||
<td className="px-3 py-2 border border-gray-200 whitespace-nowrap">{formatDateTime(e.Ende)}</td>
|
||||
<td className="px-3 py-2 border border-gray-200">{e.ArtFuehrung}</td>
|
||||
<td className="px-3 py-2 border border-gray-200 text-center">{e.Besucher}</td>
|
||||
<td className="px-3 py-2 border border-gray-200">{e.BEOs || '—'}</td>
|
||||
<td className="px-3 py-2 border border-gray-200">{e.Objekte || '—'}</td>
|
||||
<td className="px-3 py-2 border border-gray-200 max-w-xs">
|
||||
<span className="line-clamp-2">{e.Bemerkungen || ''}</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 border border-gray-200 text-center whitespace-nowrap">
|
||||
<button
|
||||
onClick={() => onEdit(e)}
|
||||
className="text-blue-600 hover:text-blue-800 mr-3 text-xs font-medium"
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeleteId(e.ID)}
|
||||
className="text-red-600 hover:text-red-800 text-xs font-medium"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{deleteId !== null && (
|
||||
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-xl p-6 max-w-sm w-full mx-4">
|
||||
<h3 className="text-lg font-semibold mb-3">Eintrag löschen?</h3>
|
||||
<p className="text-sm text-gray-600 mb-5">Dieser Eintrag wird unwiderruflich gelöscht.</p>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<button
|
||||
onClick={() => setDeleteId(null)}
|
||||
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg text-sm"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => confirmDelete(deleteId)}
|
||||
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user