feat(frontend): Oberfläche mit RRULE-Editor und Logo-Auswahl

- Vite, React 18, TypeScript und Tailwind mit dunklem Standard-Theme über
  CSS-Variablen, heller Modus umschaltbar und in localStorage gemerkt
- Anmeldung, erzwungener Passwortwechsel, Layout mit Seitenleiste
- API-Client mit stiller Token-Erneuerung, TanStack Query mit Fehler-Toasts
- Seiten Recurrences (inkl. Detail-Drawer), Transactions, Merchants, Settings
- Geführter RRULE-Editor mit Vorlagen, Expertenmodus, deutschem Klartext und
  Live-Vorschau der nächsten Termine vom preview-Endpunkt
- Firmen-Kachelgrid mit Logo, Markenfarbe und Logo-Auswahldialog samt Upload
- Beträge durchgängig in de-DE, Eingabe in beiden Schreibweisen
- 49 Tests, tsc --noEmit und eslint sauber, Produktionsbundle baut

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
moneyfy
2026-09-09 16:23:04 +02:00
co-authored by Claude Opus 5
parent 8edb69fe6a
commit 8d6fcfeb58
57 changed files with 13064 additions and 2 deletions
+69
View File
@@ -0,0 +1,69 @@
/** Anmeldung, Abmeldung und der angemeldete Benutzer. */
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ApiError, api, isUnauthorized } from "@/lib/api";
import { keys } from "@/lib/queryClient";
import { toast } from "@/store/toast";
import type { MessageResponse, User } from "@/types/api";
export function useMe() {
return useQuery({
queryKey: keys.me,
queryFn: async () => {
try {
return await api.get<User>("/me");
} catch (fehler) {
// Kein gültiges Token bedeutet schlicht: nicht angemeldet.
if (isUnauthorized(fehler)) return null;
throw fehler;
}
},
staleTime: 60_000,
retry: false,
});
}
export function useLogin() {
const client = useQueryClient();
return useMutation({
mutationFn: (daten: { username: string; password: string }) =>
api.post<User>("/auth/login", daten),
onSuccess: (benutzer) => {
client.setQueryData(keys.me, benutzer);
void client.invalidateQueries();
},
});
}
export function useLogout() {
const client = useQueryClient();
return useMutation({
mutationFn: () => api.post<MessageResponse>("/auth/logout"),
onSuccess: () => {
client.setQueryData(keys.me, null);
client.clear();
toast.info("Abgemeldet.");
},
});
}
export function useChangePassword() {
const client = useQueryClient();
return useMutation({
mutationFn: (daten: { current_password: string; new_password: string }) =>
api.post<MessageResponse>("/auth/change-password", daten),
onSuccess: () => {
// Der Passwortwechsel beendet alle Sitzungen es folgt eine neue Anmeldung.
client.setQueryData(keys.me, null);
client.clear();
toast.success("Passwort geändert.", "Bitte melde dich neu an.");
},
});
}
/** Übersetzt einen Anmeldefehler in eine Meldung für das Formular. */
export function loginErrorMessage(error: unknown): string {
if (error instanceof ApiError) return error.message;
return "Der Server ist nicht erreichbar.";
}