d3718da1f8
Passkey-Login analog zu werte-next: auf der Login-Seite "Mit Passkey anmelden", Verwaltung der eigenen Passkeys im neuen Tab Einstellungen. Das Passwort bleibt als Alternative bestehen. - lib/webauthn.ts, lib/passkeys.ts sowie API-Routen unter /api/passkey - proxy.ts: /api/passkey/authenticate ohne Session erreichbar - TabLayout: dritter Tab Einstellungen (Index-Tabs statt Route wie werte) - RP-Konfiguration ueber AUSGABEN_RP_*, da sich die .env auf dem Server mit anderen Apps teilt, die RP_ID/RP_ORIGIN bereits belegen - Eigene Tabelle ausgaben_passkeys aus demselben Grund: werte-next nutzt in der gemeinsamen Datenbank RXF bereits die Tabelle passkeys - eslint.config.mjs auf die Flat-Config von eslint-config-next umgestellt Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
96 lines
3.0 KiB
TypeScript
96 lines
3.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import AusgabenForm from '@/components/AusgabenForm';
|
|
import AusgabenList from '@/components/AusgabenList';
|
|
import MonatsStatistik from '@/components/MonatsStatistik';
|
|
import TabLayout, { SETTINGS_TAB } from '@/components/TabLayout';
|
|
import Passkeys from '@/components/Passkeys';
|
|
import { AusgabenEntry } from '@/types/ausgaben';
|
|
|
|
const MAX_ENTRIES = 15;
|
|
|
|
export default function Home() {
|
|
const [activeTab, setActiveTab] = useState(0); // 0 = Haushalt, 1 = Privat
|
|
const [entries, setEntries] = useState<AusgabenEntry[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [selectedEntry, setSelectedEntry] = useState<AusgabenEntry | null>(null);
|
|
const [statsRefreshKey, setStatsRefreshKey] = useState(0);
|
|
|
|
useEffect(() => {
|
|
setSelectedEntry(null); // Clear selected entry when switching tabs
|
|
if (activeTab === SETTINGS_TAB) return; // Einstellungen laden keine Einträge
|
|
fetchRecentEntries();
|
|
}, [activeTab]);
|
|
|
|
const fetchRecentEntries = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const response = await fetch(`/api/ausgaben?limit=${MAX_ENTRIES}&typ=${activeTab}`, {
|
|
cache: 'no-store',
|
|
headers: {
|
|
'Cache-Control': 'no-cache',
|
|
},
|
|
});
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
setEntries(data.data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching entries:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSuccess = () => {
|
|
setSelectedEntry(null);
|
|
setStatsRefreshKey((k) => k + 1);
|
|
setTimeout(() => {
|
|
fetchRecentEntries();
|
|
}, 100);
|
|
};
|
|
|
|
const handleDelete = (id: number) => {
|
|
setEntries(entries.filter(entry => entry.ID !== id));
|
|
setStatsRefreshKey((k) => k + 1);
|
|
};
|
|
|
|
const handleEdit = (entry: AusgabenEntry) => {
|
|
setSelectedEntry(entry);
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
};
|
|
|
|
if (activeTab === SETTINGS_TAB) {
|
|
return (
|
|
<TabLayout activeTab={activeTab} onTabChange={setActiveTab}>
|
|
<div>
|
|
<h2 className="text-xl font-semibold mb-4">Einstellungen</h2>
|
|
<Passkeys />
|
|
</div>
|
|
</TabLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<TabLayout activeTab={activeTab} onTabChange={setActiveTab}>
|
|
<div>
|
|
<h2 className="text-xl font-semibold mb-4">Eingabe</h2>
|
|
<AusgabenForm onSuccess={handleSuccess} selectedEntry={selectedEntry} typ={activeTab} />
|
|
|
|
<MonatsStatistik typ={activeTab} refreshKey={statsRefreshKey} />
|
|
|
|
<div className="mt-6 bg-white border border-black rounded-lg shadow-md p-6">
|
|
<h3 className="text-xl font-semibold mb-4">Letzte {MAX_ENTRIES} Einträge</h3>
|
|
{isLoading ? (
|
|
<div className="text-center py-4">Lade Daten...</div>
|
|
) : (
|
|
<AusgabenList entries={entries} onDelete={handleDelete} onEdit={handleEdit} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</TabLayout>
|
|
);
|
|
}
|