v1.4.0: Monatsfilter, Pagination, Rollenverwaltung, DB-Bereinigung
- Liste: Monatsfilter mit ←/→ Navigation, Standard = aktueller Monat - Liste: Pagination (10 Einträge/Seite) - BEO-Auswahl filtert nur role='guide' - logbuch_objekte: ObjektName entfernt, JOIN auf objekte - utf8mb4 Migration und DB-Charset-Umstellung - SSH-Tunnel-Support: MySQL auf 127.0.0.1:3336 - phpMyAdmin unter /myadmin Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+101
-35
@@ -13,6 +13,28 @@ interface Props {
|
||||
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
|
||||
const MONATE = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
|
||||
|
||||
function currentMonth() {
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;
|
||||
}
|
||||
|
||||
function monthLabel(ym: string) {
|
||||
const [y, m] = ym.split('-').map(Number);
|
||||
return `${MONATE[m - 1]} ${y}`;
|
||||
}
|
||||
|
||||
function prevMonth(ym: string) {
|
||||
const [y, m] = ym.split('-').map(Number);
|
||||
return m === 1 ? `${y - 1}-12` : `${y}-${pad(m - 1)}`;
|
||||
}
|
||||
|
||||
function nextMonth(ym: string) {
|
||||
const [y, m] = ym.split('-').map(Number);
|
||||
return m === 12 ? `${y + 1}-01` : `${y}-${pad(m + 1)}`;
|
||||
}
|
||||
|
||||
function formatDate(dt: string, short = false): string {
|
||||
if (!dt) return '';
|
||||
const d = new Date(dt);
|
||||
@@ -28,25 +50,34 @@ function formatTime(dt: string): string {
|
||||
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 20, compact = false }: Props) {
|
||||
export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, compact = false }: Props) {
|
||||
const [entries, setEntries] = useState<LogbuchEintrag[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
const [month, setMonth] = useState(compact ? '' : currentMonth());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => { setPage(0); }, [kuppel, refreshKey, month]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetch(`/api/logbuch?kuppel=${encodeURIComponent(kuppel)}&limit=${limit}`)
|
||||
const offset = page * limit;
|
||||
const url = `/api/logbuch?kuppel=${encodeURIComponent(kuppel)}&limit=${limit}&offset=${offset}` +
|
||||
(month ? `&month=${encodeURIComponent(month)}` : '');
|
||||
fetch(url)
|
||||
.then((r) => { if (!r.ok) throw new Error(); return r.json(); })
|
||||
.then((data) => { setEntries(data); setLoading(false); })
|
||||
.then((data) => { setEntries(data.entries); setTotal(data.total); setLoading(false); })
|
||||
.catch(() => { setError('Fehler beim Laden.'); setLoading(false); });
|
||||
}, [kuppel, refreshKey, limit]);
|
||||
}, [kuppel, refreshKey, limit, page, month]);
|
||||
|
||||
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));
|
||||
setTotal((t) => t - 1);
|
||||
} catch {
|
||||
setError('Fehler beim Löschen.');
|
||||
} finally {
|
||||
@@ -54,9 +85,39 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 20, co
|
||||
}
|
||||
}
|
||||
|
||||
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>;
|
||||
const monthNav = !compact && (
|
||||
<div className="flex items-center gap-2 mb-3 print:hidden">
|
||||
<button
|
||||
onClick={() => setMonth((m) => prevMonth(m))}
|
||||
className="px-2 py-1 text-sm rounded-lg bg-gray-200 hover:bg-gray-300"
|
||||
>←</button>
|
||||
<input
|
||||
type="month"
|
||||
value={month}
|
||||
onChange={(e) => setMonth(e.target.value)}
|
||||
className="border border-gray-300 rounded-lg px-2 py-1 text-sm"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setMonth((m) => nextMonth(m))}
|
||||
disabled={month >= currentMonth()}
|
||||
className="px-2 py-1 text-sm rounded-lg bg-gray-200 hover:bg-gray-300 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>→</button>
|
||||
{month !== currentMonth() && (
|
||||
<button
|
||||
onClick={() => setMonth(currentMonth())}
|
||||
className="text-sm text-blue-600 hover:underline"
|
||||
>
|
||||
Aktueller Monat
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const printHeader = !compact && (
|
||||
<div className="hidden print:block mb-3 text-sm font-semibold">
|
||||
Monat: {monthLabel(month)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const cell = compact
|
||||
? 'px-1.5 py-1 border border-gray-200 text-xs'
|
||||
@@ -65,8 +126,13 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 20, 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></>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{monthNav}
|
||||
{printHeader}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse" style={{ fontSize: compact ? '0.75rem' : '0.875rem' }}>
|
||||
<thead>
|
||||
@@ -90,7 +156,13 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 20, co
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((e) => (
|
||||
{entries.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)}.
|
||||
</td>
|
||||
</tr>
|
||||
) : entries.map((e) => (
|
||||
<tr key={e.ID} className="hover:bg-gray-50">
|
||||
<td className={`${cell} whitespace-nowrap`}>{formatDate(e.Beginn, compact)}</td>
|
||||
{compact ? (
|
||||
@@ -112,9 +184,7 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 20, co
|
||||
<td className={`${cell} text-center`}>{e.Besucher || ''}</td>
|
||||
<td className={cell}>{e.BEOs || '—'}</td>
|
||||
<td className={cell}>{e.Objekte || '—'}</td>
|
||||
{!compact && (
|
||||
<td className={cell}>{e.Bemerkungen || ''}</td>
|
||||
)}
|
||||
{!compact && <td className={cell}>{e.Bemerkungen || ''}</td>}
|
||||
{!compact && (
|
||||
<td className={cell}>
|
||||
{e.WetterTemp !== null && (
|
||||
@@ -127,18 +197,8 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 20, co
|
||||
</td>
|
||||
)}
|
||||
<td className={`${cell} text-center whitespace-nowrap print:hidden`}>
|
||||
<button
|
||||
onClick={() => onEdit(e)}
|
||||
className="text-blue-600 hover:text-blue-800 mr-2 font-medium"
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeleteId(e.ID)}
|
||||
className="text-red-600 hover:text-red-800 font-medium"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<button onClick={() => onEdit(e)} className="text-blue-600 hover:text-blue-800 mr-2 font-medium">✎</button>
|
||||
<button onClick={() => setDeleteId(e.ID)} className="text-red-600 hover:text-red-800 font-medium">✕</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -146,24 +206,30 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 20, co
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{!compact && total > limit && (
|
||||
<div className="flex items-center justify-center gap-3 mt-3 print:hidden">
|
||||
<button
|
||||
onClick={() => setPage((p) => p - 1)}
|
||||
disabled={page === 0}
|
||||
className="px-3 py-1.5 text-sm rounded-lg bg-gray-200 hover:bg-gray-300 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>← Zurück</button>
|
||||
<span className="text-sm text-gray-600">Seite {page + 1} von {Math.ceil(total / limit)}</span>
|
||||
<button
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
disabled={(page + 1) * limit >= total}
|
||||
className="px-3 py-1.5 text-sm rounded-lg bg-gray-200 hover:bg-gray-300 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>Weiter →</button>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user