Compare commits
6 Commits
c3f0b8f1e0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 978ed4e1da | |||
| 7475d4fd37 | |||
| 96ba03b909 | |||
| 421b589169 | |||
| d13e3b0ba9 | |||
| a75303f857 |
+3
-31
@@ -19,7 +19,7 @@ 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`.
|
||||
Wurde das Passwort noch nicht geändert, muss nach dem ersten Login sofort ein neues Passwort vergeben werden. Das Standard-Passwort lautet `welzheim`.
|
||||
|
||||
---
|
||||
|
||||
@@ -141,6 +141,8 @@ Zeigt eine Monatstabelle mit Anzahl der Führungen und Besuchern, aufgeschlüsse
|
||||
- Kumulierte Besucher für die gesamte Sternwarte (alle Kuppeln)
|
||||
- Führungstage für die gesamte Sternwarte
|
||||
|
||||
- **Grafik** Über diesen Button kann die (bekannte) Statistik-Grafik aufgerufen werden. Sie wird in ein einem gesonderten Fenster angezeigt. Zurück zum Führungsbuch kommt man über den Tab-Wechsel im Browser.
|
||||
|
||||
---
|
||||
|
||||
## 6. Drucken
|
||||
@@ -176,33 +178,3 @@ Zeigt alle bekannten Objekte mit ID, Name und Datum der letzten Verwendung.
|
||||
- **Objekt umbenennen**: Stift-Symbol ✎ in der Zeile anklicken, Namen ändern und mit **Speichern** bestätigen oder mit **Abbrechen** verwerfen.
|
||||
- **Objekt löschen**: × in der Zeile – es erscheint ein Bestätigungsdialog. Das Löschen ist **unwiderruflich** und entfernt das Objekt aus allen bestehenden Führungsbucheinträgen.
|
||||
|
||||
## Neue Features (Statistik Grafik Proxy)
|
||||
|
||||
Dieses Release ergänzt eine serverseitige Proxy-Lösung für das interne Statistik-Portal, damit geschützte Diagramme sicher eingeblendet werden können, ohne Zugangsdaten im Browser zu speichern.
|
||||
|
||||
- Was neu ist:
|
||||
- Server-seitiger Proxy unter `/api/statistik/grafik` und Catch-all `/api/statistik/grafik/*`.
|
||||
- Holt die Statistik-Seite mit Basic-Auth (serverseitig) und liefert sie an den Browser weiter.
|
||||
- Schreibt die HTML-Antwort so um, dass relative Assets (CSS/JS/Images) über die Proxy-URL geladen werden (es wird ein `<base href="/api/statistik/grafik/">` eingefügt).
|
||||
- Leitet auch AJAX-POSTs (z. B. `php/statistic.php`) weiter – Methoden und Bodies werden beibehalten.
|
||||
- Entfernt framing-blockierende Header (z. B. `X-Frame-Options`, CSP-Meta-Tags) in der proxied HTML-Antwort.
|
||||
|
||||
- Wichtige Environment-Variablen (nur serverseitig):
|
||||
- `STATISTIK_GRAFIK_URL` — Basis-URL des internen Statistik-Portals (z. B. `https://sternwarte-welzheim.de/intern/statistik`).
|
||||
- `STATISTIK_GRAFIK_USER` — Benutzername für Basic-Auth.
|
||||
- `STATISTIK_GRAFIK_PASS` — Passwort für Basic-Auth.
|
||||
|
||||
- UI-Änderung:
|
||||
- Der `Grafik`-Button in der Statistik-Ansicht öffnet die Statistik jetzt in einem neuen Fenster (`window.open('/api/statistik/grafik', '_blank')`). Die vorherige iframe-Integration wurde entfernt, weil manche Browser (insbesondere Firefox und Safari) Probleme mit eingebetteten, geschützten Seiten machen.
|
||||
|
||||
- Sicherheit & Hinweise:
|
||||
- Setze die `STATISTIK_...` Variablen in deiner Server-Umgebung (Docker secrets oder im Host-Umfeld). Niemals Zugangsdaten ins Repository committen.
|
||||
- Die Proxy-Route ist so konfiguriert, dass Assets und AJAX-Aufrufe über den gleichen Proxy laufen, damit die Seite vollständig funktioniert.
|
||||
|
||||
Wenn du möchtest, pushe ich die Änderungen an `ANLEITUNG.md` in `origin/main` für dich.
|
||||
|
||||
Optionales manuelles Deploy (falls Docker lokal verfügbar):
|
||||
|
||||
```bash
|
||||
./deploy.sh 1.7.8
|
||||
```
|
||||
|
||||
@@ -12,7 +12,9 @@ export default function ObjekteManager({ initialObjekte }: Props) {
|
||||
const router = useRouter();
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
const [editKategorie, setEditKategorie] = useState<string>('stern');
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newKategorie, setNewKategorie] = useState<string>('stern');
|
||||
const [error, setError] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
@@ -24,7 +26,7 @@ export default function ObjekteManager({ initialObjekte }: Props) {
|
||||
const res = await fetch('/api/objekte/' + id, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: trimmed }),
|
||||
body: JSON.stringify({ name: trimmed, kategorie: editKategorie }),
|
||||
});
|
||||
setBusy(false);
|
||||
if (!res.ok) {
|
||||
@@ -59,7 +61,7 @@ export default function ObjekteManager({ initialObjekte }: Props) {
|
||||
const res = await fetch('/api/objekte', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: trimmed }),
|
||||
body: JSON.stringify({ name: trimmed, kategorie: newKategorie }),
|
||||
});
|
||||
setBusy(false);
|
||||
if (!res.ok) {
|
||||
@@ -85,6 +87,15 @@ export default function ObjekteManager({ initialObjekte }: Props) {
|
||||
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"
|
||||
/>
|
||||
<select
|
||||
value={newKategorie}
|
||||
onChange={(e) => setNewKategorie(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
<option value="stern">Stern</option>
|
||||
<option value="sonne">Sonne</option>
|
||||
<option value="stern,sonne">Stern & Sonne</option>
|
||||
</select>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
@@ -100,6 +111,7 @@ export default function ObjekteManager({ initialObjekte }: Props) {
|
||||
<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">Kategorie</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>
|
||||
@@ -125,6 +137,27 @@ export default function ObjekteManager({ initialObjekte }: Props) {
|
||||
obj.Name
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 hidden sm:table-cell">
|
||||
{editingId === obj.ID ? (
|
||||
<select
|
||||
value={editKategorie}
|
||||
onChange={(e) => setEditKategorie(e.target.value)}
|
||||
className="px-2 py-1 border border-blue-400 rounded text-sm focus:outline-none"
|
||||
>
|
||||
<option value="stern">Stern</option>
|
||||
<option value="sonne">Sonne</option>
|
||||
<option value="stern,sonne">Stern & Sonne</option>
|
||||
</select>
|
||||
) : (
|
||||
<span className="flex gap-1">
|
||||
{obj.Kategorie.split(',').map((k) => (
|
||||
<span key={k} className={`text-xs px-2 py-0.5 rounded-full font-medium ${k === 'sonne' ? 'bg-yellow-100 text-yellow-800' : 'bg-blue-100 text-blue-800'}`}>
|
||||
{k}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-500 hidden sm:table-cell">
|
||||
{obj.LastUsed ? new Date(obj.LastUsed).toLocaleDateString('de-DE') : '—'}
|
||||
</td>
|
||||
@@ -151,7 +184,7 @@ export default function ObjekteManager({ initialObjekte }: Props) {
|
||||
<span className="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setEditingId(obj.ID); setEditName(obj.Name); setError(''); }}
|
||||
onClick={() => { setEditingId(obj.ID); setEditName(obj.Name); setEditKategorie(obj.Kategorie); 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"
|
||||
>
|
||||
@@ -172,7 +205,7 @@ export default function ObjekteManager({ initialObjekte }: Props) {
|
||||
))}
|
||||
{initialObjekte.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-4 py-6 text-center text-gray-400 text-sm">Keine Objekte vorhanden.</td>
|
||||
<td colSpan={5} className="px-4 py-6 text-center text-gray-400 text-sm">Keine Objekte vorhanden.</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
|
||||
+9
-26
@@ -2,39 +2,27 @@
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getSession } from '@/lib/session';
|
||||
import { query } from '@/lib/db';
|
||||
import * as phpdb from '@/lib/phpdb';
|
||||
|
||||
export type { BeoUser } from '@/lib/phpdb';
|
||||
|
||||
export interface ObjektRow {
|
||||
ID: number;
|
||||
Name: string;
|
||||
LastUsed: string | null;
|
||||
Kategorie: string;
|
||||
}
|
||||
|
||||
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[];
|
||||
return phpdb.listObjekteAdmin();
|
||||
}
|
||||
|
||||
export interface BeoUser {
|
||||
id: number;
|
||||
kürzel: string | null;
|
||||
name: string;
|
||||
vorname: string | null;
|
||||
role: string | null;
|
||||
hasPw: boolean;
|
||||
}
|
||||
|
||||
export async function listUsers(): Promise<BeoUser[]> {
|
||||
export async function listUsers(): Promise<phpdb.BeoUser[]> {
|
||||
const session = await getSession();
|
||||
if (!session || !session.role?.includes('admin')) redirect('/');
|
||||
|
||||
const rows = await query(
|
||||
'SELECT id, `kürzel`, name, vorname, role, (pw IS NOT NULL) AS hasPw FROM beos ORDER BY name, vorname',
|
||||
[]
|
||||
) as (Omit<BeoUser, 'hasPw'> & { hasPw: number })[];
|
||||
return rows.map(r => ({ ...r, hasPw: r.hasPw === 1 }));
|
||||
return phpdb.listUsers();
|
||||
}
|
||||
|
||||
export async function resetPassword(
|
||||
@@ -46,16 +34,11 @@ export async function resetPassword(
|
||||
return { error: 'Keine Berechtigung.' };
|
||||
}
|
||||
|
||||
const idRaw = formData.get('id');
|
||||
const id = Number(idRaw);
|
||||
const id = Number(formData.get('id'));
|
||||
if (!id || isNaN(id)) {
|
||||
return { error: 'Ungültige Benutzer-ID.' };
|
||||
}
|
||||
|
||||
await query(
|
||||
'UPDATE beos SET pw = NULL, MustChangePassword = 1 WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
await phpdb.resetBeoPassword(id);
|
||||
return { success: 'Passwort wurde zurückgesetzt. Der Benutzer muss sich mit dem Standard-Passwort anmelden und es dann ändern.' };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
const DB_URL = (process.env.PHP_DB_URL ?? 'http://localhost:8080/DB4js_all.php')
|
||||
.replace(/\?.*$/, '');
|
||||
|
||||
async function proxy(req: Request) {
|
||||
const search = new URL(req.url).search;
|
||||
const target = DB_URL + search;
|
||||
const isReadOnly = req.method === 'GET' || req.method === 'HEAD';
|
||||
const upstream = await fetch(target, {
|
||||
method: req.method,
|
||||
headers: { 'content-type': req.headers.get('content-type') ?? 'application/x-www-form-urlencoded' },
|
||||
body: isReadOnly ? undefined : await req.arrayBuffer(),
|
||||
cache: 'no-store',
|
||||
});
|
||||
const body = await upstream.arrayBuffer();
|
||||
return new NextResponse(Buffer.from(body), {
|
||||
status: upstream.status,
|
||||
headers: { 'content-type': upstream.headers.get('content-type') ?? 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
export const GET = proxy;
|
||||
export const POST = proxy;
|
||||
@@ -1,14 +1,12 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { query } from '@/lib/db';
|
||||
import { getSession } from '@/lib/session';
|
||||
import * as phpdb from '@/lib/phpdb';
|
||||
|
||||
export async function GET() {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
|
||||
try {
|
||||
const rows = await query(
|
||||
'SELECT id AS ID, `kürzel` AS Kuerzel, CONCAT(IFNULL(vorname, \'\'), IF(vorname IS NOT NULL, \' \', \'\'), name) AS Name FROM beos WHERE `kürzel` IS NOT NULL AND FIND_IN_SET(\'guide\', role) > 0 ORDER BY name ASC'
|
||||
) as { ID: number; Kuerzel: string; Name: string }[];
|
||||
const rows = await phpdb.getBeos();
|
||||
return NextResponse.json(rows);
|
||||
} catch (error) {
|
||||
console.error('GET /api/beos:', error);
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { query } from '@/lib/db';
|
||||
import { getSession } from '@/lib/session';
|
||||
import * as phpdb from '@/lib/phpdb';
|
||||
|
||||
export interface FahrkostenRow {
|
||||
ID: number;
|
||||
Kuerzel: string;
|
||||
Name: string;
|
||||
Anzahl: number;
|
||||
}
|
||||
|
||||
const EXCLUDED = "'PrF','Beob','BEOS','TD','ToT'";
|
||||
export type { FahrkostenRow } from '@/lib/phpdb';
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const session = await getSession();
|
||||
@@ -22,18 +15,7 @@ export async function GET(req: Request) {
|
||||
}
|
||||
|
||||
try {
|
||||
const rows = await query(
|
||||
'SELECT b.id AS ID, b.`kürzel` AS Kuerzel,' +
|
||||
' CONCAT(IFNULL(b.vorname, \'\'), IF(b.vorname IS NOT NULL, \' \', \'\'), b.name) AS Name,' +
|
||||
' COUNT(DISTINCT l.ID) AS Anzahl' +
|
||||
' FROM beos b' +
|
||||
' JOIN logbuch_beos lb ON lb.BeoID = b.id' +
|
||||
' JOIN logbuch l ON l.ID = lb.LogbuchID' +
|
||||
' WHERE l.Beginn >= ? AND l.ArtFuehrung NOT IN (' + EXCLUDED + ')' +
|
||||
' GROUP BY b.id, b.`kürzel`, b.name, b.vorname' +
|
||||
' ORDER BY b.name ASC',
|
||||
[ab + ' 00:00:00']
|
||||
) as FahrkostenRow[];
|
||||
const rows = await phpdb.getFahrkosten(ab);
|
||||
return NextResponse.json(rows);
|
||||
} catch (error) {
|
||||
console.error('GET /api/fahrkosten:', error);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { query, getPool } from '@/lib/db';
|
||||
import { getSession } from '@/lib/session';
|
||||
import { triggerBackup } from '@/lib/backup';
|
||||
import type { SelectedObjekt } from '@/types/logbuch';
|
||||
import * as phpdb from '@/lib/phpdb';
|
||||
|
||||
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const session = await getSession();
|
||||
@@ -12,66 +11,25 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
const logbuchId = parseInt(id);
|
||||
|
||||
try {
|
||||
const existingRows = await query('SELECT ID FROM logbuch WHERE ID = ?', [logbuchId]) as { ID: number }[];
|
||||
if (existingRows.length === 0) {
|
||||
return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
const isAdmin = session.role?.includes('admin');
|
||||
const beoRows = await query('SELECT COUNT(*) AS cnt FROM logbuch_beos WHERE LogbuchID = ? AND BeoID = ?', [logbuchId, session.beoId]) as { cnt: number }[];
|
||||
const isBeo = (beoRows[0]?.cnt ?? 0) > 0;
|
||||
|
||||
if (!isAdmin && !isBeo) {
|
||||
return NextResponse.json({ error: 'Keine Berechtigung zum Ändern dieses Eintrags' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { Kuppel, ArtFuehrung, SonderName, Beginn, Ende, Besucher, beoIds, objekte, Bemerkungen, Wetter } = body;
|
||||
|
||||
await getPool().execute(
|
||||
'UPDATE logbuch SET Kuppel=?, ArtFuehrung=?, SonderName=?, Beginn=?, Ende=?, Besucher=?,' +
|
||||
' Bemerkungen=?, WetterTemp=?, WetterFeuchte=?, WetterDruck=? WHERE ID=?',
|
||||
[
|
||||
Kuppel, ArtFuehrung, SonderName || null, Beginn, Ende,
|
||||
Besucher ?? 0,
|
||||
Bemerkungen?.slice(0, 500) || null,
|
||||
Wetter?.temp ?? null,
|
||||
Wetter?.feuchte ?? null,
|
||||
Wetter?.druck ?? null,
|
||||
logbuchId,
|
||||
]
|
||||
);
|
||||
|
||||
await query('DELETE FROM logbuch_beos WHERE LogbuchID = ?', [logbuchId]);
|
||||
await query('DELETE FROM logbuch_objekte WHERE LogbuchID = ?', [logbuchId]);
|
||||
|
||||
for (const beoId of (beoIds as number[]) || []) {
|
||||
await query('INSERT INTO logbuch_beos (LogbuchID, BeoID) VALUES (?, ?)', [logbuchId, beoId]);
|
||||
}
|
||||
|
||||
for (const obj of (objekte as SelectedObjekt[]) || []) {
|
||||
let objektId = obj.ID;
|
||||
if (!objektId) {
|
||||
const existing = await query('SELECT ID, Name FROM objekte WHERE LOWER(Name) = LOWER(?)', [obj.Name]) as { ID: number; Name: string }[];
|
||||
if (existing[0]) {
|
||||
objektId = existing[0].ID;
|
||||
} else {
|
||||
const [ins] = await getPool().execute(
|
||||
'INSERT INTO objekte (Name) VALUES (?)', [obj.Name]
|
||||
) as [{ insertId: number }, unknown];
|
||||
objektId = ins.insertId;
|
||||
}
|
||||
}
|
||||
await query('UPDATE objekte SET LastUsed = NOW() WHERE ID = ?', [objektId]);
|
||||
await query(
|
||||
'INSERT INTO logbuch_objekte (LogbuchID, ObjektID) VALUES (?, ?)',
|
||||
[logbuchId, objektId]
|
||||
);
|
||||
}
|
||||
await phpdb.updateLogbuch(logbuchId, session.beoId, session.role ?? '', {
|
||||
Kuppel, ArtFuehrung, SonderName, Beginn, Ende,
|
||||
Besucher: Besucher ?? 0,
|
||||
beoIds: beoIds ?? [],
|
||||
objekte: objekte ?? [],
|
||||
Bemerkungen: Bemerkungen ?? null,
|
||||
Wetter: Wetter ?? null,
|
||||
});
|
||||
|
||||
triggerBackup();
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('404')) return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
|
||||
if (error.message.includes('403')) return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 });
|
||||
}
|
||||
console.error('PUT /api/logbuch/[id]:', error);
|
||||
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
|
||||
}
|
||||
@@ -85,22 +43,13 @@ export async function DELETE(_request: NextRequest, { params }: { params: Promis
|
||||
const logbuchId = parseInt(id);
|
||||
|
||||
try {
|
||||
const existingRows = await query('SELECT ID FROM logbuch WHERE ID = ?', [logbuchId]) as { ID: number }[];
|
||||
if (existingRows.length === 0) {
|
||||
return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
const isAdmin = session.role?.includes('admin');
|
||||
const beoRows = await query('SELECT COUNT(*) AS cnt FROM logbuch_beos WHERE LogbuchID = ? AND BeoID = ?', [logbuchId, session.beoId]) as { cnt: number }[];
|
||||
const isBeo = (beoRows[0]?.cnt ?? 0) > 0;
|
||||
|
||||
if (!isAdmin && !isBeo) {
|
||||
return NextResponse.json({ error: 'Keine Berechtigung zum Löschen dieses Eintrags' }, { status: 403 });
|
||||
}
|
||||
|
||||
await query('DELETE FROM logbuch WHERE ID = ?', [logbuchId]);
|
||||
await phpdb.deleteLogbuch(logbuchId, session.beoId, session.role ?? '');
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('404')) return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
|
||||
if (error.message.includes('403')) return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 });
|
||||
}
|
||||
console.error('DELETE /api/logbuch/[id]:', error);
|
||||
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
|
||||
}
|
||||
|
||||
+18
-114
@@ -1,90 +1,23 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { query, getPool } from '@/lib/db';
|
||||
import { getSession } from '@/lib/session';
|
||||
import { triggerBackup } from '@/lib/backup';
|
||||
import type { SelectedObjekt } from '@/types/logbuch';
|
||||
|
||||
const LIST_SQL =
|
||||
'SELECT' +
|
||||
' l.ID, l.Kuppel, l.ArtFuehrung,' +
|
||||
" DATE_FORMAT(l.Beginn, '%Y-%m-%dT%H:%i') AS Beginn," +
|
||||
" DATE_FORMAT(l.Ende, '%Y-%m-%dT%H:%i') AS Ende," +
|
||||
' l.Besucher, l.Bemerkungen, l.SonderName,' +
|
||||
' l.WetterTemp, l.WetterFeuchte, l.WetterDruck,' +
|
||||
' l.created_by, l.created_at,' +
|
||||
' creator.kuerzel AS created_by_kuerzel,' +
|
||||
" GROUP_CONCAT(DISTINCT bk.kuerzel ORDER BY bk.kuerzel SEPARATOR ', ') AS BEOs," +
|
||||
" GROUP_CONCAT(DISTINCT o.Name ORDER BY o.Name SEPARATOR ', ') AS Objekte" +
|
||||
' FROM logbuch l' +
|
||||
' LEFT JOIN (SELECT id, `kürzel` AS kuerzel FROM beos) creator ON creator.id = l.created_by' +
|
||||
' 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';
|
||||
import * as phpdb from '@/lib/phpdb';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
|
||||
if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const kuppel = searchParams.get('kuppel') || 'West';
|
||||
const limit = Math.min(parseInt(searchParams.get('limit') || '10') || 10, 500);
|
||||
const limit = Math.min(parseInt(searchParams.get('limit') || '10') || 10, 500);
|
||||
const offset = Math.max(0, parseInt(searchParams.get('offset') || '0') || 0);
|
||||
const month = searchParams.get('month') || '';
|
||||
const order = searchParams.get('order') === 'asc' ? 'ASC' : 'DESC';
|
||||
|
||||
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];
|
||||
if (month && /^\d{4}-\d{2}$/.test(month)) {
|
||||
const [y, m] = month.split('-').map(Number);
|
||||
const start = `${y}-${String(m).padStart(2, '0')}-01`;
|
||||
const nextM = m === 12 ? 1 : m + 1;
|
||||
const nextY = m === 12 ? y + 1 : y;
|
||||
const end = `${nextY}-${String(nextM).padStart(2, '0')}-01`;
|
||||
listWhere += ' AND l.Beginn >= ? AND l.Beginn < ?';
|
||||
countWhere += ' AND Beginn >= ? AND Beginn < ?';
|
||||
params = [kuppel, start, end];
|
||||
}
|
||||
|
||||
try {
|
||||
const [countRows, entries] = await Promise.all([
|
||||
query('SELECT COUNT(*) AS total FROM logbuch ' + countWhere, params) as Promise<{ total: number }[]>,
|
||||
query(LIST_SQL.replace('WHERE l.Kuppel = ?', listWhere) + ` ORDER BY l.Beginn ${order} LIMIT ${limit} OFFSET ${offset}`, params),
|
||||
]);
|
||||
return NextResponse.json({ entries, total: (countRows as unknown as { total: number }[])[0]?.total ?? 0 });
|
||||
const result = await phpdb.listLogbuch({ kuppel, limit, offset, month, search, order });
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
console.error('GET /api/logbuch:', error);
|
||||
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
|
||||
@@ -99,47 +32,18 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
const { Kuppel, ArtFuehrung, SonderName, Beginn, Ende, Besucher, beoIds, objekte, Bemerkungen, Wetter } = body;
|
||||
|
||||
const pool = getPool();
|
||||
const [result] = await pool.execute(
|
||||
'INSERT INTO logbuch (Kuppel, ArtFuehrung, SonderName, Beginn, Ende, Besucher, Bemerkungen, WetterTemp, WetterFeuchte, WetterDruck, created_by)' +
|
||||
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
Kuppel, ArtFuehrung, SonderName || null, Beginn, Ende,
|
||||
Besucher ?? 0,
|
||||
Bemerkungen?.slice(0, 500) || null,
|
||||
Wetter?.temp ?? null,
|
||||
Wetter?.feuchte ?? null,
|
||||
Wetter?.druck ?? null,
|
||||
session.beoId,
|
||||
]
|
||||
) as [{ insertId: number }, unknown];
|
||||
|
||||
const logbuchId = result.insertId;
|
||||
|
||||
for (const beoId of (beoIds as number[]) || []) {
|
||||
await query('INSERT INTO logbuch_beos (LogbuchID, BeoID) VALUES (?, ?)', [logbuchId, beoId]);
|
||||
}
|
||||
|
||||
for (const obj of (objekte as SelectedObjekt[]) || []) {
|
||||
let objektId = obj.ID;
|
||||
if (!objektId) {
|
||||
const existing = await query('SELECT ID, Name FROM objekte WHERE LOWER(Name) = LOWER(?)', [obj.Name]) as { ID: number; Name: string }[];
|
||||
if (existing[0]) {
|
||||
objektId = existing[0].ID;
|
||||
} else {
|
||||
const [ins] = await pool.execute('INSERT INTO objekte (Name) VALUES (?)', [obj.Name]) as [{ insertId: number }, unknown];
|
||||
objektId = ins.insertId;
|
||||
}
|
||||
}
|
||||
await query('UPDATE objekte SET LastUsed = NOW() WHERE ID = ?', [objektId]);
|
||||
await query(
|
||||
'INSERT INTO logbuch_objekte (LogbuchID, ObjektID) VALUES (?, ?)',
|
||||
[logbuchId, objektId]
|
||||
);
|
||||
}
|
||||
const result = await phpdb.createLogbuch({
|
||||
Kuppel, ArtFuehrung, SonderName, Beginn, Ende,
|
||||
Besucher: Besucher ?? 0,
|
||||
beoIds: beoIds ?? [],
|
||||
objekte: objekte ?? [],
|
||||
Bemerkungen: Bemerkungen ?? null,
|
||||
Wetter: Wetter ?? null,
|
||||
created_by: session.beoId,
|
||||
});
|
||||
|
||||
triggerBackup();
|
||||
return NextResponse.json({ id: logbuchId }, { status: 201 });
|
||||
return NextResponse.json(result, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('POST /api/logbuch:', error);
|
||||
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { query } from '@/lib/db';
|
||||
import { getSession } from '@/lib/session';
|
||||
import * as phpdb from '@/lib/phpdb';
|
||||
|
||||
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const session = await getSession();
|
||||
@@ -10,11 +10,13 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
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 { name, kategorie } = 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 });
|
||||
const VALID = ['stern', 'sonne', 'stern,sonne'];
|
||||
const kat: string | undefined = VALID.includes(kategorie) ? kategorie : undefined;
|
||||
const result = await phpdb.updateObjekt(numId, trimmed, kat);
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
console.error('PUT /api/objekte/[id]:', error);
|
||||
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
|
||||
@@ -29,7 +31,7 @@ export async function DELETE(_req: NextRequest, { params }: { params: Promise<{
|
||||
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]);
|
||||
await phpdb.deleteObjekt(numId);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
console.error('DELETE /api/objekte/[id]:', error);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { query } from '@/lib/db';
|
||||
import { getSession } from '@/lib/session';
|
||||
import * as phpdb from '@/lib/phpdb';
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(req: NextRequest) {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
|
||||
try {
|
||||
const rows = await query('SELECT ID, Name FROM objekte ORDER BY LastUsed DESC LIMIT 100');
|
||||
const raw = req.nextUrl.searchParams.get('kategorie');
|
||||
const kategorie = raw === 'sonne' ? 'sonne' : 'stern';
|
||||
const rows = await phpdb.getObjekte(kategorie);
|
||||
return NextResponse.json(rows);
|
||||
} catch (error) {
|
||||
console.error('GET /api/objekte:', error);
|
||||
@@ -19,11 +21,13 @@ export async function POST(req: NextRequest) {
|
||||
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 { name, kategorie } = 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 });
|
||||
const VALID = ['stern', 'sonne', 'stern,sonne'];
|
||||
const kat: string = VALID.includes(kategorie) ? kategorie : 'stern';
|
||||
const result = await phpdb.createObjekt(trimmed, kat);
|
||||
return NextResponse.json(result, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('POST /api/objekte:', error);
|
||||
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
|
||||
|
||||
@@ -93,7 +93,17 @@ export async function grafikProxy(req: Request, slug?: string[]) {
|
||||
});
|
||||
|
||||
const contentType = upstream.headers.get('content-type') || outHeaders['content-type'] || '';
|
||||
if (contentType.toLowerCase().includes('text/html')) {
|
||||
const isHtmlResponse = contentType.toLowerCase().includes('text/html');
|
||||
|
||||
// JS/CSS/font assets that come back as HTML are upstream errors (404, redirect pages).
|
||||
// Return a 404 so the browser doesn't refuse to execute them as wrong MIME type.
|
||||
const assetExtension = /\.(js|css|woff2?|ttf|eot|png|jpg|gif|svg|ico)(\?|$)/i;
|
||||
const requestPath = new URL(req.url).pathname;
|
||||
if (isHtmlResponse && slug?.length && assetExtension.test(requestPath)) {
|
||||
return new NextResponse(null, { status: 404 });
|
||||
}
|
||||
|
||||
if (isHtmlResponse) {
|
||||
const text = Buffer.from(bodyBuf).toString('utf8');
|
||||
const cleaned = rewriteHtml(text);
|
||||
outHeaders['content-type'] = 'text/html; charset=utf-8';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { query } from '@/lib/db';
|
||||
import { getSession } from '@/lib/session';
|
||||
import * as phpdb from '@/lib/phpdb';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await getSession();
|
||||
@@ -10,66 +10,8 @@ export async function GET(request: NextRequest) {
|
||||
const year = parseInt(searchParams.get('year') || String(new Date().getFullYear()), 10);
|
||||
|
||||
try {
|
||||
const monthlyRows = await query(
|
||||
'SELECT' +
|
||||
' MONTH(Beginn) AS monat,' +
|
||||
" COUNT(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF') THEN 1 END) AS tageFuehrungen," +
|
||||
" COUNT(CASE WHEN ArtFuehrung = 'Beob' THEN 1 END) AS tageBeob," +
|
||||
" COUNT(CASE WHEN ArtFuehrung = 'TD' THEN 1 END) AS tageTD," +
|
||||
" COUNT(CASE WHEN ArtFuehrung = 'Sonst' THEN 1 END) AS tageSonst," +
|
||||
" COUNT(CASE WHEN ArtFuehrung = 'BEOS' THEN 1 END) AS tageBEOS," +
|
||||
" COUNT(CASE WHEN ArtFuehrung = 'ToT' THEN 1 END) AS tagesToT," +
|
||||
" COUNT(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF','Beob','TD','Sonst','BEOS','ToT') THEN 1 END) AS tageGesamt," +
|
||||
" SUM(CASE WHEN ArtFuehrung = 'RF' THEN Besucher ELSE 0 END) AS besucherRF," +
|
||||
" SUM(CASE WHEN ArtFuehrung = 'SF' THEN Besucher ELSE 0 END) AS besucherSF," +
|
||||
" SUM(CASE WHEN ArtFuehrung = 'SonF' THEN Besucher ELSE 0 END) AS besucherSonF," +
|
||||
" SUM(CASE WHEN ArtFuehrung = 'PrF' THEN Besucher ELSE 0 END) AS besucherPrF," +
|
||||
" SUM(CASE WHEN ArtFuehrung = 'ToT' THEN Besucher ELSE 0 END) AS besucherToT," +
|
||||
" SUM(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF','ToT') THEN Besucher ELSE 0 END) AS besucherGesamt" +
|
||||
' FROM logbuch' +
|
||||
' WHERE YEAR(Beginn) = ?' +
|
||||
' GROUP BY MONTH(Beginn)' +
|
||||
' ORDER BY monat',
|
||||
[year]
|
||||
) as {
|
||||
monat: number;
|
||||
tageFuehrungen: number; tageBeob: number; tageTD: number; tageSonst: number; tageBEOS: number; tagesToT: number; tageGesamt: number;
|
||||
besucherRF: number; besucherSF: number; besucherSonF: number; besucherPrF: number;
|
||||
besucherToT: number; besucherGesamt: number;
|
||||
}[];
|
||||
|
||||
const cumulativeRows = await query(
|
||||
"SELECT SUM(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF','ToT') THEN Besucher ELSE 0 END) AS total" +
|
||||
' FROM logbuch WHERE YEAR(Beginn) = ?',
|
||||
[year]
|
||||
) as { total: number | null }[];
|
||||
|
||||
const tageRows = await query(
|
||||
"SELECT COUNT(*) AS tage FROM logbuch WHERE YEAR(Beginn) = ? AND ArtFuehrung IN ('RF','SF','SonF','PrF','Beob','TD','Sonst','BEOS','ToT')",
|
||||
[year]
|
||||
) as { tage: number }[];
|
||||
|
||||
return NextResponse.json({
|
||||
monthly: monthlyRows.map((r) => ({
|
||||
monat: Number(r.monat),
|
||||
tageFuehrungen: Number(r.tageFuehrungen),
|
||||
tageBeob: Number(r.tageBeob),
|
||||
tageTD: Number(r.tageTD),
|
||||
tageSonst: Number(r.tageSonst),
|
||||
tageBEOS: Number(r.tageBEOS),
|
||||
tagesToT: Number(r.tagesToT),
|
||||
tageGesamt: Number(r.tageGesamt),
|
||||
besucherRF: Number(r.besucherRF),
|
||||
besucherSF: Number(r.besucherSF),
|
||||
besucherSonF: Number(r.besucherSonF),
|
||||
besucherPrF: Number(r.besucherPrF),
|
||||
besucherToT: Number(r.besucherToT),
|
||||
besucherGesamt: Number(r.besucherGesamt),
|
||||
})),
|
||||
cumulative: Number(cumulativeRows[0]?.total ?? 0),
|
||||
tage: Number(tageRows[0]?.tage ?? 0),
|
||||
year,
|
||||
});
|
||||
const result = await phpdb.getStatistik(year);
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
console.error('GET /api/statistik:', error);
|
||||
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getSession, createSession } from '@/lib/session';
|
||||
import { hashPassword } from '@/lib/auth';
|
||||
import { query } from '@/lib/db';
|
||||
import { updateBeoPassword } from '@/lib/phpdb';
|
||||
|
||||
export async function changePassword(
|
||||
_prevState: { error: string } | undefined,
|
||||
@@ -28,10 +28,7 @@ export async function changePassword(
|
||||
}
|
||||
|
||||
const hashed = await hashPassword(newPassword);
|
||||
await query(
|
||||
'UPDATE beos SET pw = ?, MustChangePassword = 0 WHERE id = ?',
|
||||
[hashed, session.beoId]
|
||||
);
|
||||
await updateBeoPassword(session.beoId, hashed);
|
||||
|
||||
await createSession({
|
||||
kuerzel: session.kuerzel,
|
||||
|
||||
+15
-18
@@ -120,8 +120,11 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
.catch(() => {});
|
||||
}
|
||||
if (editEntry && editEntry.Objekte) {
|
||||
const names = editEntry.Objekte.split(', ').map((n) => n.trim());
|
||||
fetch('/api/objekte')
|
||||
const allNames = editEntry.Objekte.split(', ').map((n) => n.trim());
|
||||
const isSonneEntry = editEntry.ArtFuehrung === SONNE_ART;
|
||||
const names = isSonneEntry ? allNames.filter((n) => n.toLowerCase() !== 'sonne') : allNames;
|
||||
const kat = isSonneEntry ? 'sonne' : 'stern';
|
||||
fetch('/api/objekte?kategorie=' + kat)
|
||||
.then((r) => r.json())
|
||||
.then((all: { ID: number; Name: string }[]) => {
|
||||
const result: SelectedObjekt[] = names.map((name) => {
|
||||
@@ -134,12 +137,10 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
}
|
||||
}, [editEntry]);
|
||||
|
||||
// Objekte-Vorauswahl je nach Art der Führung; Besucher zurücksetzen wenn nicht relevant
|
||||
// Objekte und Besucher zurücksetzen beim Wechsel der Art der Führung
|
||||
useEffect(() => {
|
||||
if (artFuehrung === SONNE_ART) {
|
||||
setObjekte([{ ID: null, Name: 'Sonne' }]);
|
||||
} else if (NO_OBJEKTE_ARTEN.includes(artFuehrung)) {
|
||||
setObjekte([]);
|
||||
setObjekte([]);
|
||||
if (NO_OBJEKTE_ARTEN.includes(artFuehrung)) {
|
||||
setBesucher('');
|
||||
}
|
||||
}, [artFuehrung]);
|
||||
@@ -172,7 +173,7 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
Ende: ende,
|
||||
Besucher: besucher === '' ? 0 : besucher,
|
||||
beoIds: beos.map((b) => b.ID),
|
||||
objekte: showObjekte ? objekte : [],
|
||||
objekte: showObjekte ? (isSonne ? [{ ID: null, Name: 'Sonne' }, ...objekte] : objekte) : [],
|
||||
Bemerkungen: bemerkungen,
|
||||
Wetter: { ...wetter, temp: parseFloat(tempRaw) || 0 },
|
||||
};
|
||||
@@ -304,16 +305,12 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
{showObjekte && (
|
||||
<div>
|
||||
<label className={labelCls}>Beobachtete Objekte</label>
|
||||
{isSonne ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center bg-green-100 text-green-800 text-sm px-3 py-1.5 rounded-full">
|
||||
Sonne
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">(bei Sonnenführung fest vorgegeben)</span>
|
||||
</div>
|
||||
) : (
|
||||
<ObjektSelector selected={objekte} onChange={setObjekte} />
|
||||
)}
|
||||
<ObjektSelector
|
||||
selected={objekte}
|
||||
onChange={setObjekte}
|
||||
kategorie={isSonne ? 'sonne' : 'stern'}
|
||||
fixedItems={isSonne ? [{ ID: null, Name: 'Sonne' }] : []}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ import type { ObjektOption, SelectedObjekt } from '@/types/logbuch';
|
||||
interface Props {
|
||||
selected: SelectedObjekt[];
|
||||
onChange: (objekte: SelectedObjekt[]) => void;
|
||||
kategorie?: 'stern' | 'sonne';
|
||||
fixedItems?: SelectedObjekt[];
|
||||
}
|
||||
|
||||
export default function ObjektSelector({ selected, onChange }: Props) {
|
||||
export default function ObjektSelector({ selected, onChange, kategorie = 'stern', fixedItems = [] }: Props) {
|
||||
const [all, setAll] = useState<ObjektOption[]>([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
@@ -16,11 +18,11 @@ export default function ObjektSelector({ selected, onChange }: Props) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/objekte')
|
||||
fetch('/api/objekte?kategorie=' + kategorie)
|
||||
.then((r) => { if (!r.ok) throw new Error('Fehler'); return r.json(); })
|
||||
.then(setAll)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
}, [kategorie]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleOutside(e: MouseEvent) {
|
||||
@@ -32,14 +34,17 @@ export default function ObjektSelector({ selected, onChange }: Props) {
|
||||
return () => document.removeEventListener('mousedown', handleOutside);
|
||||
}, [dropdownOpen]);
|
||||
|
||||
const fixedNamesLower = new Set(fixedItems.map((o) => o.Name.toLowerCase()));
|
||||
const selectedNames = new Set(selected.map((o) => o.Name.toLowerCase()));
|
||||
const available = all.filter((o) => !selectedNames.has(o.Name.toLowerCase()));
|
||||
const available = all.filter(
|
||||
(o) => !selectedNames.has(o.Name.toLowerCase()) && !fixedNamesLower.has(o.Name.toLowerCase())
|
||||
);
|
||||
const filtered = search
|
||||
? available.filter((o) => o.Name.toLowerCase().startsWith(search.toLowerCase()))
|
||||
: available;
|
||||
|
||||
const searchTrimmed = search.trim();
|
||||
const alreadySelected = searchTrimmed !== '' && selectedNames.has(searchTrimmed.toLowerCase());
|
||||
const alreadySelected = searchTrimmed !== '' && (selectedNames.has(searchTrimmed.toLowerCase()) || fixedNamesLower.has(searchTrimmed.toLowerCase()));
|
||||
const exactAvailableMatch = available.find((o) => o.Name.toLowerCase() === searchTrimmed.toLowerCase());
|
||||
const showAddNew = searchTrimmed !== '' && !alreadySelected && !exactAvailableMatch;
|
||||
|
||||
@@ -51,7 +56,7 @@ export default function ObjektSelector({ selected, onChange }: Props) {
|
||||
|
||||
function addNew(name: string) {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed || selectedNames.has(trimmed.toLowerCase())) return;
|
||||
if (!trimmed || selectedNames.has(trimmed.toLowerCase()) || fixedNamesLower.has(trimmed.toLowerCase())) return;
|
||||
const existing = all.find((o) => o.Name.toLowerCase() === trimmed.toLowerCase());
|
||||
if (existing) {
|
||||
onChange([...selected, { ID: existing.ID, Name: existing.Name }]);
|
||||
@@ -79,6 +84,14 @@ export default function ObjektSelector({ selected, onChange }: Props) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{fixedItems.map((o) => (
|
||||
<span
|
||||
key={'fixed-' + o.Name}
|
||||
className="inline-flex items-center gap-2 bg-blue-100 text-blue-800 text-base px-3 py-1.5 rounded-full"
|
||||
>
|
||||
{o.Name}
|
||||
</span>
|
||||
))}
|
||||
{selected.map((o) => (
|
||||
<span
|
||||
key={o.Name}
|
||||
|
||||
+2
-68
@@ -1,79 +1,14 @@
|
||||
services:
|
||||
logbuch_mysql:
|
||||
image: mysql:lts
|
||||
container_name: logbuch_mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
|
||||
MYSQL_DATABASE: ${DB_NAME}
|
||||
MYSQL_USER: ${DB_USER}
|
||||
MYSQL_PASSWORD: ${DB_PASS}
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
ports:
|
||||
- "127.0.0.1:3336:3306"
|
||||
networks:
|
||||
- proxy
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- mysqladmin
|
||||
- ping
|
||||
- -h
|
||||
- localhost
|
||||
- -uroot
|
||||
- -p${DB_ROOT_PASS}
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
# Kein Port nach außen — nur internes Netzwerk
|
||||
|
||||
logbuch_phpmyadmin:
|
||||
image: phpmyadmin:latest
|
||||
container_name: logbuch_phpmyadmin
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PMA_HOST: logbuch_mysql
|
||||
PMA_PORT: 3306
|
||||
PMA_ABSOLUTE_URI: https://logbuch.fuerst-stuttgart.de/myadmin/
|
||||
depends_on:
|
||||
logbuch_mysql:
|
||||
condition: service_healthy
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.logbuch-pma.entrypoints=http
|
||||
- traefik.http.routers.logbuch-pma.rule=Host(`logbuch.fuerst-stuttgart.de`) && PathPrefix(`/myadmin`)
|
||||
- traefik.http.middlewares.logbuch-pma-https-redirect.redirectscheme.scheme=https
|
||||
- traefik.http.routers.logbuch-pma.middlewares=logbuch-pma-https-redirect
|
||||
- traefik.http.routers.logbuch-pma-secure.entrypoints=https
|
||||
- traefik.http.routers.logbuch-pma-secure.rule=Host(`logbuch.fuerst-stuttgart.de`) && PathPrefix(`/myadmin`)
|
||||
- traefik.http.routers.logbuch-pma-secure.tls=true
|
||||
- traefik.http.routers.logbuch-pma-secure.middlewares=logbuch-pma-slash,logbuch-pma-strip
|
||||
- traefik.http.middlewares.logbuch-pma-slash.redirectregex.regex=^https://logbuch\.fuerst-stuttgart\.de/myadmin$$
|
||||
- traefik.http.middlewares.logbuch-pma-slash.redirectregex.replacement=https://logbuch.fuerst-stuttgart.de/myadmin/
|
||||
- traefik.http.middlewares.logbuch-pma-strip.stripprefix.prefixes=/myadmin
|
||||
- traefik.http.routers.logbuch-pma-secure.service=logbuch-pma
|
||||
- traefik.http.services.logbuch-pma.loadbalancer.server.port=80
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
logbuch_app:
|
||||
image: docker.citysensor.de/logbuch:latest
|
||||
container_name: logbuch_app
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DB_HOST: logbuch_mysql
|
||||
DB_USER: ${DB_USER}
|
||||
DB_PASS: ${DB_PASS}
|
||||
DB_NAME: ${DB_NAME}
|
||||
DB_PORT: 3306
|
||||
PHP_DB_URL: ${PHP_DB_URL}
|
||||
AUTH_SECRET: ${AUTH_SECRET}
|
||||
NODE_ENV: production
|
||||
ports:
|
||||
- 127.0.0.1:${APP_PORT:-3000}:3000
|
||||
depends_on:
|
||||
logbuch_mysql:
|
||||
condition: service_healthy
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.logbuch.entrypoints=http
|
||||
@@ -88,6 +23,7 @@ services:
|
||||
networks:
|
||||
- proxy
|
||||
- gitea-internal
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
name: dockge_default
|
||||
@@ -95,5 +31,3 @@ networks:
|
||||
gitea-internal:
|
||||
name: gitea_gitea-internal
|
||||
external: true
|
||||
volumes:
|
||||
db_data: null
|
||||
|
||||
+2
-45
@@ -1,64 +1,21 @@
|
||||
services:
|
||||
|
||||
logbuch_mysql:
|
||||
image: mysql:lts
|
||||
container_name: logbuch_mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
|
||||
MYSQL_DATABASE: ${DB_NAME}
|
||||
MYSQL_USER: ${DB_USER}
|
||||
MYSQL_PASSWORD: ${DB_PASS}
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
networks:
|
||||
- logbuch_net
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${DB_ROOT_PASS}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
# Kein Port nach außen — nur internes Netzwerk
|
||||
|
||||
logbuch_phpmyadmin:
|
||||
image: phpmyadmin:latest
|
||||
container_name: logbuch_phpmyadmin
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PMA_HOST: logbuch_mysql
|
||||
PMA_PORT: 3306
|
||||
PMA_ABSOLUTE_URI: https://logbuch.fuerst-stuttgart.de/myadmin/
|
||||
ports:
|
||||
- "127.0.0.1:${PMA_PORT:-8080}:80"
|
||||
depends_on:
|
||||
logbuch_mysql:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- logbuch_net
|
||||
|
||||
logbuch_app:
|
||||
image: docker.citysensor.de/logbuch:latest
|
||||
container_name: logbuch_app
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
DB_HOST: logbuch_mysql
|
||||
DB_PORT: 3306
|
||||
NODE_ENV: production
|
||||
BACKUP_SSH_KEY_PATH: /run/secrets/backup_ssh_key
|
||||
volumes:
|
||||
- ${BACKUP_SSH_KEY_FILE:-/dev/null}:/run/secrets/backup_ssh_key:ro
|
||||
ports:
|
||||
- "127.0.0.1:${APP_PORT:-3000}:3000"
|
||||
depends_on:
|
||||
logbuch_mysql:
|
||||
condition: service_healthy
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
networks:
|
||||
- logbuch_net
|
||||
|
||||
networks:
|
||||
logbuch_net:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
|
||||
+5
-27
@@ -1,34 +1,13 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { query } from './db';
|
||||
import { getBeoByKuerzel, getBeoByName } from './phpdb';
|
||||
|
||||
export interface Beo {
|
||||
id: number;
|
||||
name: string;
|
||||
vorname: string | null;
|
||||
kürzel: string | null;
|
||||
pw: string | null;
|
||||
MustChangePassword: number;
|
||||
role: string | null;
|
||||
}
|
||||
|
||||
export async function getBeoByKuerzel(kuerzel: string): Promise<Beo | null> {
|
||||
const rows = await query(
|
||||
'SELECT id, name, vorname, `kürzel`, pw, MustChangePassword, role FROM beos WHERE `kürzel` = ?',
|
||||
[kuerzel]
|
||||
) as Beo[];
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
export type { Beo } from './phpdb';
|
||||
import type { Beo } from './phpdb';
|
||||
|
||||
export async function getBeoByLogin(login: string): Promise<Beo | null> {
|
||||
// First try exact Kürzel match, then case-insensitive Nachname match
|
||||
const byKuerzel = await getBeoByKuerzel(login);
|
||||
if (byKuerzel) return byKuerzel;
|
||||
|
||||
const rows = await query(
|
||||
'SELECT id, name, vorname, `kürzel`, pw, MustChangePassword, role FROM beos WHERE LOWER(name) = LOWER(?)',
|
||||
[login]
|
||||
) as Beo[];
|
||||
return rows[0] ?? null;
|
||||
return getBeoByName(login);
|
||||
}
|
||||
|
||||
export async function verifyCredentials(
|
||||
@@ -41,8 +20,7 @@ export async function verifyCredentials(
|
||||
if (!beo.pw) {
|
||||
const defaultPw = process.env.DEFAULT_PASSWORD;
|
||||
if (!defaultPw) throw new Error('DEFAULT_PASSWORD Umgebungsvariable ist nicht gesetzt!');
|
||||
const valid = password === defaultPw;
|
||||
return { beo, valid };
|
||||
return { beo, valid: password === defaultPw };
|
||||
}
|
||||
|
||||
const valid = await bcrypt.compare(password, beo.pw);
|
||||
|
||||
+6
-19
@@ -2,15 +2,14 @@ import { createWriteStream, mkdirSync, unlinkSync } from 'fs';
|
||||
import { createGzip } from 'zlib';
|
||||
import { join } from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import { getPool } from './db';
|
||||
import { getBackupData } from './phpdb';
|
||||
|
||||
export function triggerBackup(): void {
|
||||
setImmediate(() => runBackup().catch((e) => console.error('[backup] Fehler:', e)));
|
||||
}
|
||||
|
||||
async function dumpToFile(filePath: string): Promise<void> {
|
||||
const dbName = process.env.DB_NAME || 'sternwarte';
|
||||
const pool = getPool();
|
||||
const { tables } = await getBackupData();
|
||||
|
||||
const gzip = createGzip();
|
||||
const file = createWriteStream(filePath);
|
||||
@@ -21,21 +20,12 @@ async function dumpToFile(filePath: string): Promise<void> {
|
||||
);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
await write(`-- Führungsbuch Backup ${now}\n-- Datenbank: ${dbName} (ohne Tabelle beos)\n\nSET FOREIGN_KEY_CHECKS=0;\n\n`);
|
||||
await write(`-- Führungsbuch Backup ${now}\n-- Logbuch-Tabellen\n\nSET FOREIGN_KEY_CHECKS=0;\n\n`);
|
||||
|
||||
const [tableRows] = await pool.query('SHOW TABLES') as [Record<string, string>[], unknown];
|
||||
const tables = tableRows
|
||||
.map((r) => Object.values(r)[0])
|
||||
.filter((t) => t !== 'beos');
|
||||
|
||||
for (const table of tables) {
|
||||
const [[createRow]] = await pool.query(`SHOW CREATE TABLE \`${table}\``) as [Record<string, string>[], unknown];
|
||||
const createSql = Object.values(createRow)[1];
|
||||
|
||||
await write(`DROP TABLE IF EXISTS \`${table}\`;\n`);
|
||||
for (const { name, createSql, rows } of tables) {
|
||||
await write(`DROP TABLE IF EXISTS \`${name}\`;\n`);
|
||||
await write(`${createSql};\n\n`);
|
||||
|
||||
const [rows] = await pool.query(`SELECT * FROM \`${table}\``) as [Record<string, unknown>[], unknown];
|
||||
if (rows.length > 0) {
|
||||
const cols = Object.keys(rows[0]).map((c) => `\`${c}\``).join(', ');
|
||||
const batchSize = 200;
|
||||
@@ -45,11 +35,10 @@ async function dumpToFile(filePath: string): Promise<void> {
|
||||
'(' + Object.values(row).map((v) => {
|
||||
if (v === null) return 'NULL';
|
||||
if (typeof v === 'number') return String(v);
|
||||
if (v instanceof Date) return `'${v.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
||||
return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
||||
}).join(', ') + ')'
|
||||
).join(',\n ');
|
||||
await write(`INSERT INTO \`${table}\` (${cols}) VALUES\n ${values};\n`);
|
||||
await write(`INSERT INTO \`${name}\` (${cols}) VALUES\n ${values};\n`);
|
||||
}
|
||||
await write('\n');
|
||||
}
|
||||
@@ -99,7 +88,6 @@ async function runBackup(): Promise<void> {
|
||||
'-o', 'ConnectTimeout=15',
|
||||
];
|
||||
|
||||
// Zielverzeichnis auf Remote anlegen falls nicht vorhanden
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const ssh = spawn('ssh', [...sshOpts, sshHost, `mkdir -p ${remotePath}`]);
|
||||
ssh.on('error', reject);
|
||||
@@ -118,7 +106,6 @@ async function runBackup(): Promise<void> {
|
||||
|
||||
console.log(`[backup] ${filename} → ${sshHost}:${remotePath}`);
|
||||
|
||||
// Backups älter als 30 Tage auf Remote löschen
|
||||
await new Promise<void>((resolve) => {
|
||||
const ssh = spawn('ssh', [
|
||||
...sshOpts, sshHost,
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import type { QueryResult } from 'mysql2/promise';
|
||||
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || 'mydbase_mysql',
|
||||
port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_NAME || 'logbuch',
|
||||
charset: 'utf8mb4',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
};
|
||||
|
||||
let pool: mysql.Pool | null = null;
|
||||
|
||||
export function getPool() {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool(dbConfig);
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
export async function query(sql: string, params?: (string | number | null)[]): Promise<QueryResult> {
|
||||
const p = getPool();
|
||||
const [rows] = await p.execute(sql, params || []);
|
||||
return rows as QueryResult;
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
import type { BeoOption, LogbuchEintrag, ObjektOption, SelectedObjekt, Wetter } from '@/types/logbuch';
|
||||
|
||||
const PHP_DB_URL = process.env.PHP_DB_URL ?? 'http://localhost:8080/DB4js_all.php';
|
||||
|
||||
async function call<T>(cmd: string, params: object = {}): Promise<T> {
|
||||
const res = await fetch(PHP_DB_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ cmd, ...params }),
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (!res.ok) {
|
||||
let detail = '';
|
||||
try { detail = await res.text(); } catch { /* ignore */ }
|
||||
throw new Error(`DB4js ${cmd} HTTP ${res.status}: ${detail}`);
|
||||
}
|
||||
const json: unknown = await res.json();
|
||||
if (json && typeof json === 'object' && 'error' in json) {
|
||||
throw new Error(`DB4js ${cmd}: ${(json as { error: string }).error}`);
|
||||
}
|
||||
return json as T;
|
||||
}
|
||||
|
||||
// ---- Auth / Benutzer ----
|
||||
|
||||
export interface Beo {
|
||||
id: number;
|
||||
name: string;
|
||||
vorname: string | null;
|
||||
'kürzel': string | null;
|
||||
pw: string | null;
|
||||
MustChangePassword: number;
|
||||
role: string | null;
|
||||
}
|
||||
|
||||
export interface BeoUser {
|
||||
id: number;
|
||||
'kürzel': string | null;
|
||||
name: string;
|
||||
vorname: string | null;
|
||||
role: string | null;
|
||||
hasPw: boolean;
|
||||
}
|
||||
|
||||
export async function getBeoByKuerzel(kuerzel: string): Promise<Beo | null> {
|
||||
const r = await call<{ beo: Beo | null }>('LB_AUTH_KUERZEL', { kuerzel });
|
||||
return r.beo;
|
||||
}
|
||||
|
||||
export async function getBeoByName(name: string): Promise<Beo | null> {
|
||||
const r = await call<{ beo: Beo | null }>('LB_AUTH_NAME', { name });
|
||||
return r.beo;
|
||||
}
|
||||
|
||||
export async function updateBeoPassword(id: number, pwHash: string): Promise<void> {
|
||||
await call('LB_UPDATE_PW', { id, pw: pwHash });
|
||||
}
|
||||
|
||||
export async function resetBeoPassword(id: number): Promise<void> {
|
||||
await call('LB_RESET_PW', { id });
|
||||
}
|
||||
|
||||
export async function listUsers(): Promise<BeoUser[]> {
|
||||
return call<BeoUser[]>('LB_LIST_USERS');
|
||||
}
|
||||
|
||||
// ---- Logbuch ----
|
||||
|
||||
export interface ListLogbuchParams {
|
||||
kuppel?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
month?: string;
|
||||
search?: string;
|
||||
order?: string;
|
||||
}
|
||||
|
||||
export async function listLogbuch(
|
||||
params: ListLogbuchParams
|
||||
): Promise<{ entries: LogbuchEintrag[]; total: number }> {
|
||||
return call('LB_LIST_LOGBUCH', params);
|
||||
}
|
||||
|
||||
export interface CreateLogbuchData {
|
||||
Kuppel: string;
|
||||
ArtFuehrung: string;
|
||||
SonderName?: string | null;
|
||||
Beginn: string;
|
||||
Ende: string;
|
||||
Besucher?: number;
|
||||
beoIds?: number[];
|
||||
objekte?: SelectedObjekt[];
|
||||
Bemerkungen?: string | null;
|
||||
Wetter?: Partial<Wetter> | null;
|
||||
created_by: number;
|
||||
}
|
||||
|
||||
export async function createLogbuch(data: CreateLogbuchData): Promise<{ id: number }> {
|
||||
return call('LB_CREATE_LOGBUCH', data);
|
||||
}
|
||||
|
||||
export async function updateLogbuch(
|
||||
id: number,
|
||||
userId: number,
|
||||
userRole: string,
|
||||
data: Omit<CreateLogbuchData, 'created_by'>
|
||||
): Promise<void> {
|
||||
await call('LB_UPDATE_LOGBUCH', { id, user_id: userId, user_role: userRole, ...data });
|
||||
}
|
||||
|
||||
export async function deleteLogbuch(
|
||||
id: number,
|
||||
userId: number,
|
||||
userRole: string
|
||||
): Promise<void> {
|
||||
await call('LB_DELETE_LOGBUCH', { id, user_id: userId, user_role: userRole });
|
||||
}
|
||||
|
||||
// ---- BEOs & Objekte ----
|
||||
|
||||
export async function getBeos(): Promise<BeoOption[]> {
|
||||
return call('LB_GET_BEOS');
|
||||
}
|
||||
|
||||
export async function getObjekte(kategorie: 'stern' | 'sonne' = 'stern'): Promise<ObjektOption[]> {
|
||||
return call('LB_GET_OBJEKTE', { kategorie });
|
||||
}
|
||||
|
||||
export async function createObjekt(name: string, kategorie: string = 'stern'): Promise<ObjektOption> {
|
||||
return call('LB_CREATE_OBJEKT', { name, kategorie });
|
||||
}
|
||||
|
||||
export async function updateObjekt(id: number, name: string, kategorie?: string): Promise<ObjektOption> {
|
||||
return call('LB_UPDATE_OBJEKT', { id, name, ...(kategorie ? { kategorie } : {}) });
|
||||
}
|
||||
|
||||
export async function deleteObjekt(id: number): Promise<void> {
|
||||
await call('LB_DELETE_OBJEKT', { id });
|
||||
}
|
||||
|
||||
export async function listObjekteAdmin(): Promise<{ ID: number; Name: string; LastUsed: string | null; Kategorie: string }[]> {
|
||||
return call('LB_LIST_OBJEKTE_ADMIN');
|
||||
}
|
||||
|
||||
// ---- Auswertungen ----
|
||||
|
||||
export interface FahrkostenRow {
|
||||
ID: number;
|
||||
Kuerzel: string;
|
||||
Name: string;
|
||||
Anzahl: number;
|
||||
}
|
||||
|
||||
export async function getFahrkosten(ab: string): Promise<FahrkostenRow[]> {
|
||||
return call('LB_FAHRKOSTEN', { ab });
|
||||
}
|
||||
|
||||
export interface StatistikResult {
|
||||
monthly: {
|
||||
monat: number;
|
||||
tageFuehrungen: number; tageBeob: number; tageTD: number;
|
||||
tageSonst: number; tageBEOS: number; tagesToT: number; tageGesamt: number;
|
||||
besucherRF: number; besucherSF: number; besucherSonF: number;
|
||||
besucherPrF: number; besucherToT: number; besucherGesamt: number;
|
||||
}[];
|
||||
cumulative: number;
|
||||
tage: number;
|
||||
year: number;
|
||||
}
|
||||
|
||||
export async function getStatistik(year: number): Promise<StatistikResult> {
|
||||
return call('LB_STATISTIK', { year });
|
||||
}
|
||||
|
||||
// ---- Backup ----
|
||||
|
||||
export interface BackupTable {
|
||||
name: string;
|
||||
createSql: string;
|
||||
rows: Record<string, string | number | null>[];
|
||||
}
|
||||
|
||||
export async function getBackupData(): Promise<{ tables: BackupTable[] }> {
|
||||
return call('LB_BACKUP_DATA');
|
||||
}
|
||||
+2
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "logbuch",
|
||||
"version": "1.9.1",
|
||||
"version": "1.10.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -11,8 +11,7 @@
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"jose": "^6.2.2",
|
||||
"mysql2": "^3.22.3",
|
||||
"next": "16.1.6",
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user