Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a34fccc35 | |||
| cf038ad3be | |||
| 4593713042 | |||
| edb324719b |
@@ -40,3 +40,4 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
backup.git/
|
||||
DB_BACKUP/
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { ObjektRow } from './actions';
|
||||
|
||||
interface Props {
|
||||
initialObjekte: ObjektRow[];
|
||||
}
|
||||
|
||||
export default function ObjekteManager({ initialObjekte }: Props) {
|
||||
const router = useRouter();
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
const [newName, setNewName] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function handleSaveEdit(id: number) {
|
||||
const trimmed = editName.trim();
|
||||
if (!trimmed) { setError('Name darf nicht leer sein.'); return; }
|
||||
setBusy(true);
|
||||
setError('');
|
||||
const res = await fetch('/api/objekte/' + id, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: trimmed }),
|
||||
});
|
||||
setBusy(false);
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
setError(data.error ?? 'Fehler beim Speichern.');
|
||||
return;
|
||||
}
|
||||
setEditingId(null);
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number, name: string) {
|
||||
if (!confirm(`Objekt „${name}" wirklich löschen?`)) return;
|
||||
setBusy(true);
|
||||
setError('');
|
||||
const res = await fetch('/api/objekte/' + id, { method: 'DELETE' });
|
||||
setBusy(false);
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
setError(data.error ?? 'Fehler beim Löschen.');
|
||||
return;
|
||||
}
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
async function handleCreate(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const trimmed = newName.trim();
|
||||
if (!trimmed) { setError('Name darf nicht leer sein.'); return; }
|
||||
setBusy(true);
|
||||
setError('');
|
||||
const res = await fetch('/api/objekte', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: trimmed }),
|
||||
});
|
||||
setBusy(false);
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
setError(data.error ?? 'Fehler beim Erstellen.');
|
||||
return;
|
||||
}
|
||||
setNewName('');
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{error && (
|
||||
<p className="mb-3 text-sm text-red-600 font-medium">{error}</p>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleCreate} className="flex gap-2 mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
placeholder="Neues Objekt…"
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
className="px-4 py-2 text-sm font-medium bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="bg-white border border-gray-300 rounded-xl shadow-sm overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-100 text-gray-700">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 font-semibold w-16">ID</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">Name</th>
|
||||
<th className="text-left px-4 py-3 font-semibold hidden sm:table-cell">Zuletzt verwendet</th>
|
||||
<th className="px-4 py-3 w-36"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{initialObjekte.map((obj, idx) => (
|
||||
<tr key={obj.ID} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<td className="px-4 py-2 text-gray-400 font-mono text-xs">{obj.ID}</td>
|
||||
<td className="px-4 py-2">
|
||||
{editingId === obj.ID ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleSaveEdit(obj.ID);
|
||||
if (e.key === 'Escape') setEditingId(null);
|
||||
}}
|
||||
autoFocus
|
||||
className="w-full px-2 py-1 border border-blue-400 rounded text-sm focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
obj.Name
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-500 hidden sm:table-cell">
|
||||
{obj.LastUsed ? new Date(obj.LastUsed).toLocaleDateString('de-DE') : '—'}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right">
|
||||
{editingId === obj.ID ? (
|
||||
<span className="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSaveEdit(obj.ID)}
|
||||
disabled={busy}
|
||||
className="px-2 py-1 text-xs font-medium bg-green-100 text-green-700 border border-green-300 rounded hover:bg-green-200 disabled:opacity-50"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingId(null)}
|
||||
className="px-2 py-1 text-xs font-medium bg-gray-100 text-gray-600 border border-gray-300 rounded hover:bg-gray-200"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setEditingId(obj.ID); setEditName(obj.Name); setError(''); }}
|
||||
disabled={busy}
|
||||
className="px-2 py-1 text-xs font-medium bg-blue-100 text-blue-700 border border-blue-300 rounded hover:bg-blue-200 disabled:opacity-50"
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDelete(obj.ID, obj.Name)}
|
||||
disabled={busy}
|
||||
className="px-2 py-1 text-xs font-medium bg-red-100 text-red-700 border border-red-300 rounded hover:bg-red-200 disabled:opacity-50"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{initialObjekte.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-4 py-6 text-center text-gray-400 text-sm">Keine Objekte vorhanden.</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,19 @@ import { redirect } from 'next/navigation';
|
||||
import { getSession } from '@/lib/session';
|
||||
import { query } from '@/lib/db';
|
||||
|
||||
export interface ObjektRow {
|
||||
ID: number;
|
||||
Name: string;
|
||||
LastUsed: string | null;
|
||||
}
|
||||
|
||||
export async function listObjekte(): Promise<ObjektRow[]> {
|
||||
const session = await getSession();
|
||||
if (!session || !session.role?.includes('admin')) redirect('/');
|
||||
const rows = await query('SELECT ID, Name, LastUsed FROM objekte ORDER BY Name ASC', []);
|
||||
return rows as ObjektRow[];
|
||||
}
|
||||
|
||||
export interface BeoUser {
|
||||
id: number;
|
||||
kürzel: string | null;
|
||||
|
||||
+84
-42
@@ -1,64 +1,106 @@
|
||||
import Link from 'next/link';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getSession } from '@/lib/session';
|
||||
import { listUsers } from './actions';
|
||||
import { listUsers, listObjekte } from './actions';
|
||||
import ResetButton from './ResetButton';
|
||||
import ObjekteManager from './ObjekteManager';
|
||||
|
||||
export default async function AdminPage() {
|
||||
type Tab = 'benutzer' | 'objekte';
|
||||
|
||||
export default async function AdminPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ tab?: string }>;
|
||||
}) {
|
||||
const session = await getSession();
|
||||
if (!session) redirect('/login');
|
||||
if (session.role === null || !session.role.includes('admin')) redirect('/');
|
||||
|
||||
const users = await listUsers();
|
||||
const { tab: tabParam } = await searchParams;
|
||||
const activeTab: Tab = tabParam === 'objekte' ? 'objekte' : 'benutzer';
|
||||
|
||||
const [users, objekte] = await Promise.all([
|
||||
activeTab === 'benutzer' ? listUsers() : Promise.resolve([]),
|
||||
activeTab === 'objekte' ? listObjekte() : Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const tabs: { id: Tab; label: string }[] = [
|
||||
{ id: 'benutzer', label: 'Benutzerverwaltung' },
|
||||
{ id: 'objekte', label: 'Objektverwaltung' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white py-4 px-4">
|
||||
<main className="max-w-6xl mx-auto border-2 border-black rounded-lg p-6 bg-[#EEF4FF]">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h1 className="text-3xl font-bold">Logbuch — Sternwarte Welzheim</h1>
|
||||
<Link href="/" className="text-sm text-blue-600 hover:underline">← Zurück</Link>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Benutzerverwaltung</h2>
|
||||
|
||||
<div className="bg-white border border-gray-300 rounded-xl shadow-sm overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-100 text-gray-700">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 font-semibold">Kürzel</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">Name</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">Vorname</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">Rolle</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">Passwort</th>
|
||||
<th className="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user, idx) => (
|
||||
<tr key={user.id} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<td className="px-4 py-3 font-mono">{user.kürzel ?? '—'}</td>
|
||||
<td className="px-4 py-3">{user.name}</td>
|
||||
<td className="px-4 py-3">{user.vorname ?? '—'}</td>
|
||||
<td className="px-4 py-3">{user.role ?? '—'}</td>
|
||||
<td className="px-4 py-3">
|
||||
{user.hasPw ? (
|
||||
<span className="text-green-700">gesetzt</span>
|
||||
) : (
|
||||
<span className="text-amber-600 font-medium">Standard</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<ResetButton userId={user.id} userName={`${user.vorname ?? ''} ${user.name}`.trim()} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 mb-6 border-b border-gray-200">
|
||||
{tabs.map(({ id, label }) => (
|
||||
<Link
|
||||
key={id}
|
||||
href={`/admin?tab=${id}`}
|
||||
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
||||
activeTab === id
|
||||
? 'border-[#85B7D7] text-gray-900'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-xs text-gray-500">
|
||||
„Zurücksetzen“ setzt das Passwort auf NULL. Der Benutzer muss sich danach mit dem Standard-Passwort anmelden und es sofort ändern.
|
||||
</p>
|
||||
{/* Benutzerverwaltung */}
|
||||
{activeTab === 'benutzer' && (
|
||||
<>
|
||||
<div className="bg-white border border-gray-300 rounded-xl shadow-sm overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-100 text-gray-700">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 font-semibold">Kürzel</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">Name</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">Vorname</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">Rolle</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">Passwort</th>
|
||||
<th className="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user, idx) => (
|
||||
<tr key={user.id} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<td className="px-4 py-3 font-mono">{user.kürzel ?? '—'}</td>
|
||||
<td className="px-4 py-3">{user.name}</td>
|
||||
<td className="px-4 py-3">{user.vorname ?? '—'}</td>
|
||||
<td className="px-4 py-3">{user.role ?? '—'}</td>
|
||||
<td className="px-4 py-3">
|
||||
{user.hasPw ? (
|
||||
<span className="text-green-700">gesetzt</span>
|
||||
) : (
|
||||
<span className="text-amber-600 font-medium">Standard</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<ResetButton userId={user.id} userName={`${user.vorname ?? ''} ${user.name}`.trim()} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p className="mt-4 text-xs text-gray-500">
|
||||
„Zurücksetzen“ setzt das Passwort auf NULL. Der Benutzer muss sich danach mit dem Standard-Passwort anmelden und es sofort ändern.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Objektverwaltung */}
|
||||
{activeTab === 'objekte' && (
|
||||
<ObjekteManager initialObjekte={objekte} />
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { query } from '@/lib/db';
|
||||
import { getSession } from '@/lib/session';
|
||||
|
||||
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
|
||||
if (!session.role?.includes('admin')) return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 });
|
||||
try {
|
||||
const { id } = await params;
|
||||
const numId = Number(id);
|
||||
if (isNaN(numId)) return NextResponse.json({ error: 'Ungültige ID' }, { status: 400 });
|
||||
const { name } = await req.json();
|
||||
const trimmed = (name as string)?.trim();
|
||||
if (!trimmed) return NextResponse.json({ error: 'Name darf nicht leer sein' }, { status: 400 });
|
||||
await query('UPDATE objekte SET Name = ? WHERE ID = ?', [trimmed, numId]);
|
||||
return NextResponse.json({ ID: numId, Name: trimmed });
|
||||
} catch (error) {
|
||||
console.error('PUT /api/objekte/[id]:', error);
|
||||
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
|
||||
if (!session.role?.includes('admin')) return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 });
|
||||
try {
|
||||
const { id } = await params;
|
||||
const numId = Number(id);
|
||||
if (isNaN(numId)) return NextResponse.json({ error: 'Ungültige ID' }, { status: 400 });
|
||||
await query('DELETE FROM objekte WHERE ID = ?', [numId]);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
console.error('DELETE /api/objekte/[id]:', error);
|
||||
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { query } from '@/lib/db';
|
||||
import { getSession } from '@/lib/session';
|
||||
|
||||
@@ -13,3 +13,19 @@ export async function GET() {
|
||||
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
|
||||
if (!session.role?.includes('admin')) return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 });
|
||||
try {
|
||||
const { name } = await req.json();
|
||||
const trimmed = (name as string)?.trim();
|
||||
if (!trimmed) return NextResponse.json({ error: 'Name darf nicht leer sein' }, { status: 400 });
|
||||
const result = await query('INSERT INTO objekte (Name) VALUES (?)', [trimmed]) as { insertId: number };
|
||||
return NextResponse.json({ ID: result.insertId, Name: trimmed }, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('POST /api/objekte:', error);
|
||||
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
+33
-24
@@ -20,10 +20,18 @@ function toLocalDatetimeValue(isoOrDatetime: string): string {
|
||||
return isoOrDatetime.slice(0, 16);
|
||||
}
|
||||
|
||||
function nowLocalDatetime(): string {
|
||||
function todayDate(): string {
|
||||
const now = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
||||
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
||||
}
|
||||
|
||||
function nowRounded5(): string {
|
||||
const now = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
const total = now.getHours() * 60 + now.getMinutes();
|
||||
const rounded = Math.ceil(total / 5) * 5;
|
||||
return `${pad(Math.floor(rounded / 60) % 24)}:${pad(rounded % 60)}`;
|
||||
}
|
||||
|
||||
const NO_OBJEKTE_ARTEN: ArtFuehrung[] = ['BEOS', 'TD'];
|
||||
@@ -31,14 +39,15 @@ const SONNE_ART: ArtFuehrung = 'SonF';
|
||||
|
||||
export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved }: Props) {
|
||||
const [artFuehrung, setArtFuehrung] = useState<ArtFuehrung>('RF');
|
||||
const [beginn, setBeginn] = useState(nowLocalDatetime());
|
||||
const [ende, setEnde] = useState(nowLocalDatetime());
|
||||
const [beginn, setBeginn] = useState(todayDate());
|
||||
const [ende, setEnde] = useState(todayDate() + 'T' + nowRounded5());
|
||||
const [besucher, setBesucher] = useState<number | ''>('');
|
||||
const [beos, setBeos] = useState<BeoOption[]>([currentUserBeo]);
|
||||
const [objekte, setObjekte] = useState<SelectedObjekt[]>([]);
|
||||
const [bemerkungen, setBemerkungen] = useState('');
|
||||
const [sonderName, setSonderName] = useState('');
|
||||
const [wetter, setWetter] = useState<Wetter>({ temp: 0, feuchte: 0, druck: 0 });
|
||||
const [tempRaw, setTempRaw] = useState('0');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
@@ -68,6 +77,7 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
.then((w: Wetter) => {
|
||||
if (!editEntryRef.current) {
|
||||
setWetter({ temp: w.temp, feuchte: Math.round(w.feuchte), druck: Math.round(w.druck) });
|
||||
setTempRaw(String(w.temp));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
@@ -82,16 +92,14 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
setBemerkungen(editEntry.Bemerkungen ?? '');
|
||||
setSonderName(editEntry.SonderName ?? '');
|
||||
if (editEntry.WetterTemp !== null) {
|
||||
setWetter({
|
||||
temp: editEntry.WetterTemp ?? 0,
|
||||
feuchte: Math.round(editEntry.WetterFeuchte ?? 0),
|
||||
druck: Math.round(editEntry.WetterDruck ?? 0),
|
||||
});
|
||||
const t = editEntry.WetterTemp ?? 0;
|
||||
setWetter({ temp: t, feuchte: Math.round(editEntry.WetterFeuchte ?? 0), druck: Math.round(editEntry.WetterDruck ?? 0) });
|
||||
setTempRaw(String(t));
|
||||
}
|
||||
} else {
|
||||
setArtFuehrung('RF');
|
||||
setBeginn(nowLocalDatetime());
|
||||
setEnde(nowLocalDatetime());
|
||||
setBeginn(todayDate());
|
||||
setEnde(todayDate() + 'T' + nowRounded5());
|
||||
setBesucher(0);
|
||||
setBeos([currentUserBeo]);
|
||||
setObjekte([]);
|
||||
@@ -144,8 +152,12 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
setError('Bitte Besucherzahl eingeben.');
|
||||
return;
|
||||
}
|
||||
if (!beginn.slice(11, 16)) {
|
||||
setError('Bitte Startzeit eingeben.');
|
||||
return;
|
||||
}
|
||||
if (beginn === ende) {
|
||||
setError('Die Zeite wurden nicht eingegeben');
|
||||
setError('Start- und Endzeit sind identisch.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,7 +173,7 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
beoIds: beos.map((b) => b.ID),
|
||||
objekte: showObjekte ? objekte : [],
|
||||
Bemerkungen: bemerkungen,
|
||||
Wetter: wetter,
|
||||
Wetter: { ...wetter, temp: parseFloat(tempRaw) || 0 },
|
||||
};
|
||||
|
||||
const url = editEntry ? `/api/logbuch/${editEntry.ID}` : '/api/logbuch';
|
||||
@@ -177,8 +189,8 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 5000);
|
||||
if (!editEntry) {
|
||||
setBeginn(nowLocalDatetime());
|
||||
setEnde(nowLocalDatetime());
|
||||
setBeginn(todayDate());
|
||||
setEnde(todayDate() + 'T' + nowRounded5());
|
||||
setBesucher(0);
|
||||
setBeos([currentUserBeo]);
|
||||
setObjekte([]);
|
||||
@@ -238,10 +250,7 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
<label className={labelCls}>Startzeit</label>
|
||||
<TimeInput
|
||||
value={beginn.slice(11, 16)}
|
||||
onChange={(t) => {
|
||||
setBeginn(beginn.slice(0, 10) + 'T' + t);
|
||||
setEnde(ende.slice(0, 10) + 'T' + t);
|
||||
}}
|
||||
onChange={(t) => setBeginn(beginn.slice(0, 10) + 'T' + t)}
|
||||
className="w-24"
|
||||
/>
|
||||
</div>
|
||||
@@ -250,7 +259,7 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
<TimeInput
|
||||
value={ende.slice(11, 16)}
|
||||
onChange={(t) => setEnde(ende.slice(0, 10) + 'T' + t)}
|
||||
clearOnFocus
|
||||
autoFocus
|
||||
className="w-24"
|
||||
/>
|
||||
</div>
|
||||
@@ -328,10 +337,10 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-0.5">Temperatur (°C)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={wetter.temp}
|
||||
onChange={(e) => setWetter({ ...wetter, temp: parseFloat(e.target.value) || 0 })}
|
||||
step="0.1"
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={tempRaw}
|
||||
onChange={(e) => setTempRaw(e.target.value)}
|
||||
className="w-1/3 px-2 py-1 border-2 border-gray-400 rounded-lg bg-white text-sm text-gray-900 focus:border-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -117,7 +117,8 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
|
||||
<input
|
||||
type="month"
|
||||
value={month}
|
||||
onChange={(e) => setMonth(e.target.value)}
|
||||
max={currentMonth()}
|
||||
onChange={(e) => setMonth(e.target.value > currentMonth() ? currentMonth() : e.target.value)}
|
||||
className="border border-gray-300 rounded-lg px-2 py-1 text-sm"
|
||||
/>
|
||||
<button
|
||||
|
||||
@@ -13,6 +13,7 @@ export default function ObjektSelector({ selected, onChange }: Props) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/objekte')
|
||||
@@ -45,7 +46,7 @@ export default function ObjektSelector({ selected, onChange }: Props) {
|
||||
function add(obj: ObjektOption) {
|
||||
onChange([...selected, { ID: obj.ID, Name: obj.Name }]);
|
||||
setSearch('');
|
||||
setDropdownOpen(false);
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
|
||||
function addNew(name: string) {
|
||||
@@ -58,7 +59,7 @@ export default function ObjektSelector({ selected, onChange }: Props) {
|
||||
onChange([...selected, { ID: null, Name: trimmed }]);
|
||||
}
|
||||
setSearch('');
|
||||
setDropdownOpen(false);
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
|
||||
function remove(name: string) {
|
||||
@@ -98,6 +99,7 @@ export default function ObjektSelector({ selected, onChange }: Props) {
|
||||
|
||||
<div ref={wrapperRef} className="relative">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => { setSearch(e.target.value); setDropdownOpen(true); }}
|
||||
@@ -124,7 +126,7 @@ export default function ObjektSelector({ selected, onChange }: Props) {
|
||||
onClick={() => addNew(searchTrimmed)}
|
||||
className="w-full text-left px-4 py-2 text-sm text-blue-700 hover:bg-blue-50 active:bg-blue-100 border-t border-gray-200 font-medium"
|
||||
>
|
||||
+ „{searchTrimmed}" hinzufügen
|
||||
+ „{searchTrimmed}“ hinzufügen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ interface Props {
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
clearOnFocus?: boolean;
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
function isValid(t: string): boolean {
|
||||
@@ -20,7 +21,7 @@ function normalize(t: string): string {
|
||||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export default function TimeInput({ value, onChange, className = '', clearOnFocus = false }: Props) {
|
||||
export default function TimeInput({ value, onChange, className = '', clearOnFocus = false, autoFocus = false }: Props) {
|
||||
const [local, setLocal] = useState(value);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
@@ -71,6 +72,7 @@ export default function TimeInput({ value, onChange, className = '', clearOnFocu
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
autoFocus={autoFocus}
|
||||
placeholder="HH:MM"
|
||||
maxLength={5}
|
||||
className={`w-full px-2 py-1 border-2 rounded-lg bg-white text-sm text-gray-900 font-mono text-center focus:outline-none ${
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "logbuch",
|
||||
"version": "1.7.1",
|
||||
"version": "1.7.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "logbuch",
|
||||
"version": "1.7.1",
|
||||
"version": "1.7.2",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"jose": "^6.2.2",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "logbuch",
|
||||
"version": "1.7.1",
|
||||
"version": "1.7.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Reference in New Issue
Block a user