2026-03-30 16:37:58 +02:00
|
|
|
// API-Base: In Produktion relativ, in Dev mit Port
|
|
|
|
|
const BASE = typeof window !== 'undefined'
|
|
|
|
|
? (window.location.port === '5173'
|
|
|
|
|
? `http://${window.location.hostname}:8099/api` // Dev
|
|
|
|
|
: '/api') // Produktion
|
|
|
|
|
: '/api';
|
|
|
|
|
|
|
|
|
|
async function get<T>(path: string): Promise<T> {
|
|
|
|
|
const res = await fetch(`${BASE}${path}`);
|
|
|
|
|
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface VorlageKurz {
|
|
|
|
|
id: number;
|
|
|
|
|
aktenzeichen: string | null;
|
|
|
|
|
typ: string | null;
|
|
|
|
|
betreff: string | null;
|
|
|
|
|
datum_eingang: string | null;
|
|
|
|
|
ist_verwaltungsvorlage: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ParteiOut {
|
|
|
|
|
id: number;
|
|
|
|
|
kuerzel: string;
|
|
|
|
|
name: string | null;
|
|
|
|
|
farbe: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface GremiumOut {
|
|
|
|
|
id: number;
|
|
|
|
|
name: string;
|
|
|
|
|
kuerzel: string | null;
|
|
|
|
|
typ: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface BeratungOut {
|
|
|
|
|
id: number;
|
|
|
|
|
gremium: GremiumOut | null;
|
|
|
|
|
sitzung_datum: string | null;
|
|
|
|
|
rolle: string | null;
|
|
|
|
|
ergebnis: string | null;
|
|
|
|
|
ergebnis_text: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ReferenzOut {
|
|
|
|
|
vorlage_id: number;
|
|
|
|
|
aktenzeichen: string | null;
|
|
|
|
|
betreff: string | null;
|
|
|
|
|
vorlage_typ: string | null;
|
|
|
|
|
datum_eingang: string | null;
|
|
|
|
|
ref_typ: string | null;
|
|
|
|
|
konfidenz: number | null;
|
|
|
|
|
kontext: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface VorlageDetail extends VorlageKurz {
|
|
|
|
|
aktenzeichen_basis: string | null;
|
|
|
|
|
aktenzeichen_suffix: string | null;
|
|
|
|
|
volltext_clean: string | null;
|
|
|
|
|
pdf_url: string | null;
|
|
|
|
|
web_url: string | null;
|
|
|
|
|
thema_kurz: string | null;
|
|
|
|
|
antragsteller: ParteiOut[];
|
|
|
|
|
beratungen: BeratungOut[];
|
|
|
|
|
referenzen_ausgehend: ReferenzOut[];
|
|
|
|
|
referenzen_eingehend: ReferenzOut[];
|
|
|
|
|
kette_id: number | null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 00:36:30 +02:00
|
|
|
export interface AmpelSchritt {
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
aktiv: boolean;
|
|
|
|
|
erreicht: boolean;
|
|
|
|
|
farbe: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AmpelAbzweigung {
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
farbe: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AmpelData {
|
|
|
|
|
strang: string;
|
|
|
|
|
strang_label: string;
|
|
|
|
|
kontrollfrage: string | null;
|
|
|
|
|
schritte: AmpelSchritt[];
|
|
|
|
|
abzweigung: AmpelAbzweigung | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AmpelKompakt {
|
|
|
|
|
schritt: string;
|
|
|
|
|
farbe: string;
|
|
|
|
|
ist_abzweigung: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 16:37:58 +02:00
|
|
|
export interface KetteKurz {
|
|
|
|
|
id: number;
|
|
|
|
|
ursprung: VorlageKurz | null;
|
|
|
|
|
typ: string | null;
|
|
|
|
|
thema: string | null;
|
|
|
|
|
status: string | null;
|
|
|
|
|
status_seit: string | null;
|
|
|
|
|
letzte_aktivitaet: string | null;
|
|
|
|
|
vertagungen_count: number;
|
|
|
|
|
glieder_count: number;
|
2026-04-02 00:36:30 +02:00
|
|
|
strang: string | null;
|
|
|
|
|
ampel: AmpelKompakt | null;
|
2026-03-30 16:37:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KettenGliedOut {
|
|
|
|
|
vorlage: VorlageKurz;
|
|
|
|
|
position: number;
|
|
|
|
|
rolle: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KetteDetail {
|
|
|
|
|
id: number;
|
|
|
|
|
ursprung: VorlageKurz | null;
|
|
|
|
|
typ: string | null;
|
|
|
|
|
thema: string | null;
|
|
|
|
|
status: string | null;
|
|
|
|
|
status_seit: string | null;
|
|
|
|
|
letzte_aktivitaet: string | null;
|
|
|
|
|
vertagungen_count: number;
|
2026-04-01 10:36:22 +02:00
|
|
|
begruendung: string | null;
|
2026-03-30 16:37:58 +02:00
|
|
|
glieder: KettenGliedOut[];
|
|
|
|
|
antragsteller: ParteiOut[];
|
|
|
|
|
graph: {
|
|
|
|
|
nodes: GraphNode[];
|
|
|
|
|
edges: GraphEdge[];
|
|
|
|
|
} | null;
|
2026-04-02 00:36:30 +02:00
|
|
|
strang: string | null;
|
|
|
|
|
ampel: AmpelData | null;
|
2026-03-30 16:37:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface GraphNode {
|
|
|
|
|
id: number;
|
|
|
|
|
aktenzeichen: string | null;
|
|
|
|
|
typ: string | null;
|
|
|
|
|
betreff: string | null;
|
|
|
|
|
datum_eingang: string | null;
|
|
|
|
|
position?: number;
|
|
|
|
|
rolle?: string;
|
|
|
|
|
ist_verwaltungsvorlage?: boolean;
|
|
|
|
|
extern?: boolean;
|
|
|
|
|
beratungen?: { sitzung_datum: string; rolle: string; ergebnis: string; gremium_name: string }[];
|
|
|
|
|
antragsteller?: { kuerzel: string; name: string; farbe: string }[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface GraphEdge {
|
|
|
|
|
quelle_id: number;
|
|
|
|
|
ziel_id: number;
|
|
|
|
|
typ: string;
|
|
|
|
|
konfidenz: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface Paginated<T> {
|
|
|
|
|
items: T[];
|
|
|
|
|
total: number;
|
|
|
|
|
page: number;
|
|
|
|
|
page_size: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface Stats {
|
|
|
|
|
vorlagen_total: number;
|
|
|
|
|
beratungen_total: number;
|
|
|
|
|
ketten_total: number;
|
|
|
|
|
vorlagen_nach_typ: { typ: string; anzahl: number }[];
|
|
|
|
|
ketten_nach_status: { status: string; anzahl: number }[];
|
|
|
|
|
ketten_nach_typ: { typ: string; anzahl: number }[];
|
|
|
|
|
letzte_vorlagen: VorlageKurz[];
|
|
|
|
|
parteien: { kuerzel: string; name: string; farbe: string | null; anzahl: number }[];
|
|
|
|
|
gremien: { name: string; kuerzel: string | null; typ: string | null; anzahl: number }[];
|
|
|
|
|
timeline: { monat: string; anzahl: number }[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KettenStats {
|
|
|
|
|
nach_typ: Record<string, { status: string; anzahl: number }[]>;
|
|
|
|
|
status_detail: { status: string; anzahl: number; avg_tage: number; avg_vertagungen: number }[];
|
|
|
|
|
versandungs_fruehwarnung: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// API functions
|
|
|
|
|
export const fetchStats = () => get<Stats>('/stats');
|
|
|
|
|
export const fetchKettenStats = () => get<KettenStats>('/stats/ketten-stats');
|
|
|
|
|
|
|
|
|
|
export const fetchVorlagen = (params: Record<string, string>) => {
|
|
|
|
|
const qs = new URLSearchParams(params).toString();
|
|
|
|
|
return get<Paginated<VorlageKurz>>(`/vorlagen?${qs}`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const fetchVorlage = (id: number) => get<VorlageDetail>(`/vorlagen/${id}`);
|
|
|
|
|
|
|
|
|
|
export const fetchKetten = (params: Record<string, string>) => {
|
|
|
|
|
const qs = new URLSearchParams(params).toString();
|
|
|
|
|
return get<Paginated<KetteKurz>>(`/ketten?${qs}`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const fetchKette = (id: number) => get<KetteDetail>(`/ketten/${id}`);
|
2026-04-01 10:36:22 +02:00
|
|
|
|
|
|
|
|
// Fraktionen
|
|
|
|
|
export interface FraktionDashboard {
|
|
|
|
|
partei: { id: number; kuerzel: string; name: string; farbe: string | null };
|
|
|
|
|
total_antraege: number;
|
|
|
|
|
bewertet: number;
|
|
|
|
|
umsetzung: { bewertung: string; anzahl: number; avg_score: number }[];
|
|
|
|
|
antraege: {
|
|
|
|
|
id: number; aktenzeichen: string | null; betreff: string | null;
|
|
|
|
|
typ: string | null; datum_eingang: string | null;
|
|
|
|
|
umsetzung_score: number | null; umsetzung_bewertung: string | null;
|
|
|
|
|
umsetzung_begruendung: string | null;
|
|
|
|
|
}[];
|
|
|
|
|
jahre: string[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Re-evaluation
|
|
|
|
|
async function post<T>(path: string, body: object): Promise<T> {
|
|
|
|
|
const res = await fetch(`${BASE}${path}`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const reevalVorlage = (id: number, anmerkung: string) =>
|
|
|
|
|
post<{ job_id: string; status: string }>(`/bewertung/vorlagen/${id}`, { anmerkung });
|
|
|
|
|
|
|
|
|
|
export const reevalKette = (id: number, anmerkung: string) =>
|
|
|
|
|
post<{ job_id: string; status: string }>(`/bewertung/ketten/${id}`, { anmerkung });
|
|
|
|
|
|
|
|
|
|
export const fetchJobStatus = (jobId: string) =>
|
|
|
|
|
get<{ status: string; result?: object; error?: string }>(`/bewertung/status/${jobId}`);
|
|
|
|
|
|
2026-04-01 13:52:51 +02:00
|
|
|
export interface SuchVorschlag {
|
|
|
|
|
id: number;
|
|
|
|
|
aktenzeichen: string | null;
|
|
|
|
|
betreff: string | null;
|
|
|
|
|
typ: string | null;
|
|
|
|
|
snippet?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fetchSuchvorschlaege = (q: string) =>
|
|
|
|
|
get<{ items: SuchVorschlag[] }>(`/vorlagen/suggest?q=${encodeURIComponent(q)}`);
|
|
|
|
|
|
2026-04-01 10:36:22 +02:00
|
|
|
export const fetchFraktionen = () => get<{ id: number; kuerzel: string; name: string; farbe: string | null; anzahl: number }[]>('/fraktionen');
|
2026-04-02 00:36:30 +02:00
|
|
|
export interface AmpelDefinition {
|
|
|
|
|
straenge: Record<string, {
|
|
|
|
|
label: string;
|
|
|
|
|
kontrollfrage: string | null;
|
|
|
|
|
schritte: { id: string; label: string; endfarbe: string | null }[];
|
|
|
|
|
}>;
|
|
|
|
|
abzweigungen: Record<string, { label: string; farbe: string }>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fetchAmpelDefinition = () => get<AmpelDefinition>('/ampel/definition');
|
|
|
|
|
|
2026-04-01 14:58:10 +02:00
|
|
|
export const fetchFraktionDashboard = (kuerzel: string, jahr?: string, periode?: string) => {
|
|
|
|
|
const p = new URLSearchParams();
|
|
|
|
|
if (jahr) p.set('jahr', jahr);
|
|
|
|
|
if (periode) p.set('periode', periode);
|
|
|
|
|
const qs = p.toString();
|
|
|
|
|
return get<FraktionDashboard>(`/fraktionen/${kuerzel}/dashboard${qs ? `?${qs}` : ''}`);
|
2026-04-01 10:36:22 +02:00
|
|
|
};
|