Files
open-eur-buchhaltung/web/src/lib/api.ts
T
duffyduckandClaude Opus 5 bf371c8118 Einnahmen-Überschuss-Rechnung mit ELSTER, DATEV und Mandantenfähigkeit
Gewinnermittlung nach §4 Abs. 3 EStG für Einzelunternehmen und Freiberufler.
Node/Fastify mit SQLite im Backend, React/Vite im Frontend; die Anwendung
läuft lokal ohne Cloud-Anbindung.

Fachlicher Kern ist eine Zeitachse der Besteuerungsform: das umsatzsteuerliche
Regime wird je Buchung aus dem Leistungsdatum aufgelöst, statt aus einer
globalen Ja/Nein-Flagge. Dadurch ist auch ein unterjähriger Wechsel von der
Kleinunternehmerregelung zur Regelbesteuerung abbildbar, wie ihn §19 Abs. 1
Satz 3 UStG seit 2025 beim Überschreiten der 100.000-EUR-Grenze vorsieht.

Enthalten:
- Buchungserfassung nach dem Zu-/Abflussprinzip mit Storno statt Löschung
- Anlagevermögen mit AfA (linear, GWG, Sammelposten); die Aktivierung
  entscheidet das Konto, die Methode der Betrag
- Anlage EÜR, BWA im Aufbau der Form 01, Umsatzsteuer-Voranmeldung
- ELSTER-Anbindung über ERiC mit validierendem Simulationsmodus
- DATEV-Buchungsstapel (EXTF 700) und GoBD-Datenträgerüberlassung (Z3)
- Kontenrahmen SKR03 und SKR04, je Mandant beim Anlegen wählbar
- Mandantenfähigkeit mit einer SQLite-Datei je Mandant; Testmandanten sind
  von der Übermittlung ans Finanzamt ausgeschlossen

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 21:45:52 +02:00

74 lines
2.1 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react';
import { aktiverMandant } from './mandant';
export class ApiFehler extends Error {
constructor(
message: string,
public status: number,
) {
super(message);
}
}
async function anfrage<T>(pfad: string, init?: RequestInit): Promise<T> {
// Jeder Aufruf trägt den aktiven Mandanten; ohne Angabe wählt der Server.
const slug = aktiverMandant();
const antwort = await fetch(pfad, {
...init,
headers: {
'Content-Type': 'application/json',
...(slug ? { 'X-Mandant': slug } : {}),
...(init?.headers ?? {}),
},
});
const text = await antwort.text();
const daten = text ? JSON.parse(text) : null;
if (!antwort.ok) {
throw new ApiFehler(daten?.fehler ?? `Fehler ${antwort.status}`, antwort.status);
}
return daten as T;
}
export const api = {
get: <T,>(pfad: string) => anfrage<T>(pfad),
post: <T,>(pfad: string, body?: unknown) =>
anfrage<T>(pfad, { method: 'POST', body: JSON.stringify(body ?? {}) }),
put: <T,>(pfad: string, body: unknown) =>
anfrage<T>(pfad, { method: 'PUT', body: JSON.stringify(body) }),
del: <T,>(pfad: string) => anfrage<T>(pfad, { method: 'DELETE' }),
};
export function useDaten<T>(pfad: string | null): {
daten: T | null;
fehler: string | null;
laedt: boolean;
neuLaden: () => void;
} {
const [daten, setDaten] = useState<T | null>(null);
const [fehler, setFehler] = useState<string | null>(null);
const [laedt, setLaedt] = useState(true);
const [zaehler, setZaehler] = useState(0);
useEffect(() => {
if (!pfad) return;
let abgebrochen = false;
setLaedt(true);
api
.get<T>(pfad)
.then((d) => {
if (!abgebrochen) {
setDaten(d);
setFehler(null);
}
})
.catch((e: Error) => !abgebrochen && setFehler(e.message))
.finally(() => !abgebrochen && setLaedt(false));
return () => {
abgebrochen = true;
};
}, [pfad, zaehler]);
const neuLaden = useCallback(() => setZaehler((z) => z + 1), []);
return { daten, fehler, laedt, neuLaden };
}