feat: Frontend-Formulare fuer Entscheidungen und Normen anlegen
- /entscheidungen/new: Formular fuer neue Entscheidungen (Typ, Gericht, Aktenzeichen, Datum, Leitsatz, Tenor, Sachverhalt, Gruende, Volltext, Rechtsgebiete, Schlagwoerter) - /normen/new: Formular fuer neue Regelwerke mit optionalen Paragraphen inline - POST /api/norms: Neue API-Route fuer normInstrument-Erstellung - Buttons auf den Listenseiten /entscheidungen und /normen Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
262
src/app/(dashboard)/entscheidungen/new/page.tsx
Normal file
262
src/app/(dashboard)/entscheidungen/new/page.tsx
Normal file
@@ -0,0 +1,262 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
|
||||
const DECISION_TYPES = [
|
||||
{ value: 'schiedsspruch', label: 'Schiedsspruch' },
|
||||
{ value: 'urteil', label: 'Urteil' },
|
||||
{ value: 'beschluss', label: 'Beschluss' },
|
||||
{ value: 'vergleich', label: 'Vergleich' },
|
||||
{ value: 'einstweilige_verfuegung', label: 'Einstweilige Verfügung' },
|
||||
];
|
||||
|
||||
export default function NewDecisionPage() {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
const form = new FormData(e.currentTarget);
|
||||
const domainsRaw = (form.get('domains') as string) || '';
|
||||
const keywordsRaw = (form.get('keywords') as string) || '';
|
||||
|
||||
const body = {
|
||||
type: form.get('type'),
|
||||
court: form.get('court'),
|
||||
caseReference: form.get('caseReference') || undefined,
|
||||
decisionDate: form.get('decisionDate'),
|
||||
chamber: form.get('chamber') || undefined,
|
||||
headnote: form.get('headnote') || undefined,
|
||||
tenor: form.get('tenor') || undefined,
|
||||
facts: form.get('facts') || undefined,
|
||||
reasoning: form.get('reasoning') || undefined,
|
||||
fullText: form.get('fullText') || undefined,
|
||||
domains: domainsRaw ? domainsRaw.split(',').map((s) => s.trim()).filter(Boolean) : [],
|
||||
keywords: keywordsRaw ? keywordsRaw.split(',').map((s) => s.trim()).filter(Boolean) : [],
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/decisions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null);
|
||||
throw new Error(data?.error ?? 'Entscheidung konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
router.push(`/entscheidungen/${data.decision.id}`);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten.');
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const inputClass =
|
||||
'w-full rounded-lg border border-card-border px-3 py-2.5 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary';
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2 text-sm text-muted">
|
||||
<Link href="/entscheidungen" className="hover:text-primary transition-colors">
|
||||
Entscheidungen
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span>Neue Entscheidung</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-xl font-bold text-foreground">Neue Entscheidung anlegen</h1>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 rounded-lg px-4 py-3 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="bg-card-bg border border-card-border rounded-xl p-6 space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="type" className="block text-sm font-medium text-foreground mb-1">
|
||||
Typ *
|
||||
</label>
|
||||
<select id="type" name="type" required className={inputClass}>
|
||||
<option value="">Bitte waehlen...</option>
|
||||
{DECISION_TYPES.map((t) => (
|
||||
<option key={t.value} value={t.value}>
|
||||
{t.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="court" className="block text-sm font-medium text-foreground mb-1">
|
||||
Gericht *
|
||||
</label>
|
||||
<input
|
||||
id="court"
|
||||
name="court"
|
||||
type="text"
|
||||
required
|
||||
placeholder="z.B. Buehnenoberschiedsgericht"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label htmlFor="caseReference" className="block text-sm font-medium text-foreground mb-1">
|
||||
Aktenzeichen
|
||||
</label>
|
||||
<input
|
||||
id="caseReference"
|
||||
name="caseReference"
|
||||
type="text"
|
||||
placeholder="z.B. BOSchG 3/2024"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="decisionDate" className="block text-sm font-medium text-foreground mb-1">
|
||||
Datum *
|
||||
</label>
|
||||
<input id="decisionDate" name="decisionDate" type="date" required className={inputClass} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="chamber" className="block text-sm font-medium text-foreground mb-1">
|
||||
Kammer / Senat
|
||||
</label>
|
||||
<input
|
||||
id="chamber"
|
||||
name="chamber"
|
||||
type="text"
|
||||
placeholder="z.B. 3. Kammer"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="headnote" className="block text-sm font-medium text-foreground mb-1">
|
||||
Leitsatz
|
||||
</label>
|
||||
<textarea
|
||||
id="headnote"
|
||||
name="headnote"
|
||||
rows={3}
|
||||
placeholder="Leitsatz der Entscheidung"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="tenor" className="block text-sm font-medium text-foreground mb-1">
|
||||
Tenor
|
||||
</label>
|
||||
<textarea
|
||||
id="tenor"
|
||||
name="tenor"
|
||||
rows={3}
|
||||
placeholder="Tenor / Urteilsformel"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="facts" className="block text-sm font-medium text-foreground mb-1">
|
||||
Sachverhalt
|
||||
</label>
|
||||
<textarea
|
||||
id="facts"
|
||||
name="facts"
|
||||
rows={4}
|
||||
placeholder="Tatbestand / Sachverhalt"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="reasoning" className="block text-sm font-medium text-foreground mb-1">
|
||||
Entscheidungsgruende
|
||||
</label>
|
||||
<textarea
|
||||
id="reasoning"
|
||||
name="reasoning"
|
||||
rows={4}
|
||||
placeholder="Entscheidungsgruende"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="fullText" className="block text-sm font-medium text-foreground mb-1">
|
||||
Volltext
|
||||
</label>
|
||||
<textarea
|
||||
id="fullText"
|
||||
name="fullText"
|
||||
rows={6}
|
||||
placeholder="Volltext der Entscheidung (optional)"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="domains" className="block text-sm font-medium text-foreground mb-1">
|
||||
Rechtsgebiete
|
||||
</label>
|
||||
<input
|
||||
id="domains"
|
||||
name="domains"
|
||||
type="text"
|
||||
placeholder="z.B. Buehnenrecht, Arbeitsrecht (kommagetrennt)"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="keywords" className="block text-sm font-medium text-foreground mb-1">
|
||||
Schlagwoerter
|
||||
</label>
|
||||
<input
|
||||
id="keywords"
|
||||
name="keywords"
|
||||
type="text"
|
||||
placeholder="z.B. Kuendigungsschutz, NV Buehne (kommagetrennt)"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="px-4 py-2.5 bg-primary text-white rounded-lg text-sm font-medium hover:bg-primary-light transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Wird angelegt...' : 'Entscheidung anlegen'}
|
||||
</button>
|
||||
<Link
|
||||
href="/entscheidungen"
|
||||
className="px-4 py-2.5 border border-card-border rounded-lg text-sm font-medium text-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
Abbrechen
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -49,10 +49,18 @@ export default async function EntscheidungenPage({
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<p className="text-sm text-muted">
|
||||
Entscheidungsdatenbank für Bühnenrecht, Schiedssprüche und Arbeitsgerichtsurteile.
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted">
|
||||
Entscheidungsdatenbank für Bühnenrecht, Schiedssprüche und Arbeitsgerichtsurteile.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/entscheidungen/new"
|
||||
className="px-4 py-2.5 bg-primary text-white rounded-lg text-sm font-medium hover:bg-primary-light transition-colors"
|
||||
>
|
||||
Neue Entscheidung
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<form className="flex gap-2">
|
||||
|
||||
324
src/app/(dashboard)/normen/new/page.tsx
Normal file
324
src/app/(dashboard)/normen/new/page.tsx
Normal file
@@ -0,0 +1,324 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
|
||||
const NORM_TYPES = [
|
||||
{ value: 'gesetz', label: 'Gesetz' },
|
||||
{ value: 'tarifvertrag', label: 'Tarifvertrag' },
|
||||
{ value: 'schiedsordnung', label: 'Schiedsordnung' },
|
||||
{ value: 'verordnung', label: 'Verordnung' },
|
||||
{ value: 'satzung', label: 'Satzung' },
|
||||
];
|
||||
|
||||
const SOURCE_RANKS = [
|
||||
{ value: 'gesetz', label: 'Gesetz' },
|
||||
{ value: 'tarif', label: 'Tarifvertrag' },
|
||||
{ value: 'schiedsordnung', label: 'Schiedsordnung' },
|
||||
{ value: 'praxis', label: 'Praxis' },
|
||||
{ value: 'kommentar', label: 'Kommentar' },
|
||||
];
|
||||
|
||||
interface Provision {
|
||||
paragraph: string;
|
||||
title: string;
|
||||
body: string;
|
||||
validFrom: string;
|
||||
}
|
||||
|
||||
export default function NewNormPage() {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [provisions, setProvisions] = useState<Provision[]>([]);
|
||||
|
||||
function addProvision() {
|
||||
setProvisions((prev) => [
|
||||
...prev,
|
||||
{ paragraph: '', title: '', body: '', validFrom: new Date().toISOString().split('T')[0] },
|
||||
]);
|
||||
}
|
||||
|
||||
function removeProvision(index: number) {
|
||||
setProvisions((prev) => prev.filter((_, i) => i !== index));
|
||||
}
|
||||
|
||||
function updateProvision(index: number, field: keyof Provision, value: string) {
|
||||
setProvisions((prev) =>
|
||||
prev.map((p, i) => (i === index ? { ...p, [field]: value } : p)),
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
const form = new FormData(e.currentTarget);
|
||||
const instrumentBody = {
|
||||
type: form.get('type'),
|
||||
sourceRank: form.get('sourceRank'),
|
||||
abbreviation: form.get('abbreviation'),
|
||||
fullTitle: form.get('fullTitle'),
|
||||
enactedAt: form.get('enactedAt') || undefined,
|
||||
issuingBody: form.get('issuingBody') || undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/norms', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(instrumentBody),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null);
|
||||
throw new Error(data?.error ?? 'Regelwerk konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const instrumentId = data.instrument.id;
|
||||
|
||||
if (provisions.length > 0) {
|
||||
const validProvisions = provisions.filter((p) => p.paragraph && p.body && p.validFrom);
|
||||
if (validProvisions.length > 0) {
|
||||
const importRes = await fetch('/api/norms/import', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
instrumentId,
|
||||
provisions: validProvisions,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!importRes.ok) {
|
||||
const importData = await importRes.json().catch(() => null);
|
||||
throw new Error(
|
||||
importData?.error ?? 'Normen konnten nicht importiert werden.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
router.push(`/normen/${instrumentId}`);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten.');
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const inputClass =
|
||||
'w-full rounded-lg border border-card-border px-3 py-2.5 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary';
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2 text-sm text-muted">
|
||||
<Link href="/normen" className="hover:text-primary transition-colors">
|
||||
Normen
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span>Neues Regelwerk</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-xl font-bold text-foreground">Neues Regelwerk anlegen</h1>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 rounded-lg px-4 py-3 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="bg-card-bg border border-card-border rounded-xl p-6 space-y-4">
|
||||
<h2 className="text-sm font-semibold text-foreground">Regelwerk</h2>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="type" className="block text-sm font-medium text-foreground mb-1">
|
||||
Typ *
|
||||
</label>
|
||||
<select id="type" name="type" required className={inputClass}>
|
||||
<option value="">Bitte waehlen...</option>
|
||||
{NORM_TYPES.map((t) => (
|
||||
<option key={t.value} value={t.value}>
|
||||
{t.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="sourceRank" className="block text-sm font-medium text-foreground mb-1">
|
||||
Quellenrang *
|
||||
</label>
|
||||
<select id="sourceRank" name="sourceRank" required className={inputClass}>
|
||||
<option value="">Bitte waehlen...</option>
|
||||
{SOURCE_RANKS.map((r) => (
|
||||
<option key={r.value} value={r.value}>
|
||||
{r.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="abbreviation" className="block text-sm font-medium text-foreground mb-1">
|
||||
Abkuerzung *
|
||||
</label>
|
||||
<input
|
||||
id="abbreviation"
|
||||
name="abbreviation"
|
||||
type="text"
|
||||
required
|
||||
placeholder="z.B. NV Buehne"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="fullTitle" className="block text-sm font-medium text-foreground mb-1">
|
||||
Vollstaendiger Titel *
|
||||
</label>
|
||||
<input
|
||||
id="fullTitle"
|
||||
name="fullTitle"
|
||||
type="text"
|
||||
required
|
||||
placeholder="z.B. Normalvertrag Buehne"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="enactedAt" className="block text-sm font-medium text-foreground mb-1">
|
||||
Inkrafttreten
|
||||
</label>
|
||||
<input id="enactedAt" name="enactedAt" type="date" className={inputClass} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="issuingBody" className="block text-sm font-medium text-foreground mb-1">
|
||||
Herausgeber
|
||||
</label>
|
||||
<input
|
||||
id="issuingBody"
|
||||
name="issuingBody"
|
||||
type="text"
|
||||
placeholder="z.B. GDBA/DBV"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card-bg border border-card-border rounded-xl p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-foreground">
|
||||
Paragraphen / Normen <span className="font-normal text-muted">(optional)</span>
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addProvision}
|
||||
className="px-3 py-1.5 text-xs font-medium text-primary border border-primary/30 rounded-lg hover:bg-primary/5 transition-colors"
|
||||
>
|
||||
+ Paragraph hinzufuegen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{provisions.length === 0 ? (
|
||||
<p className="text-sm text-muted">
|
||||
Noch keine Paragraphen hinzugefuegt. Sie koennen Paragraphen auch spaeter ueber die API importieren.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{provisions.map((prov, idx) => (
|
||||
<div key={idx} className="border border-card-border rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-muted">Paragraph {idx + 1}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeProvision(idx)}
|
||||
className="text-xs text-red-500 hover:text-red-700 transition-colors"
|
||||
>
|
||||
Entfernen
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-foreground mb-1">
|
||||
Bezeichnung *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={prov.paragraph}
|
||||
onChange={(e) => updateProvision(idx, 'paragraph', e.target.value)}
|
||||
placeholder="z.B. § 1"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-foreground mb-1">
|
||||
Titel
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={prov.title}
|
||||
onChange={(e) => updateProvision(idx, 'title', e.target.value)}
|
||||
placeholder="z.B. Geltungsbereich"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-foreground mb-1">
|
||||
Gueltig ab *
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={prov.validFrom}
|
||||
onChange={(e) => updateProvision(idx, 'validFrom', e.target.value)}
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-foreground mb-1">
|
||||
Normtext *
|
||||
</label>
|
||||
<textarea
|
||||
value={prov.body}
|
||||
onChange={(e) => updateProvision(idx, 'body', e.target.value)}
|
||||
rows={3}
|
||||
placeholder="Vollstaendiger Normtext"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="px-4 py-2.5 bg-primary text-white rounded-lg text-sm font-medium hover:bg-primary-light transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Wird angelegt...' : 'Regelwerk anlegen'}
|
||||
</button>
|
||||
<Link
|
||||
href="/normen"
|
||||
className="px-4 py-2.5 border border-card-border rounded-lg text-sm font-medium text-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
Abbrechen
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -43,6 +43,12 @@ export default async function NormenPage() {
|
||||
Rechtsquellen nach Quellenrang geordnet. Höherrangige Normen gehen vor.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/normen/new"
|
||||
className="px-4 py-2.5 bg-primary text-white rounded-lg text-sm font-medium hover:bg-primary-light transition-colors"
|
||||
>
|
||||
Neues Regelwerk
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -56,7 +62,9 @@ export default async function NormenPage() {
|
||||
{instruments.length === 0 ? (
|
||||
<div className="bg-card-bg border border-card-border rounded-xl p-8 text-center">
|
||||
<p className="text-muted text-sm">Noch keine Normen importiert.</p>
|
||||
<p className="text-xs text-muted mt-1">Verwenden Sie die API zum Importieren: POST /api/norms/import</p>
|
||||
<p className="text-xs text-muted mt-1">
|
||||
<Link href="/normen/new" className="text-primary hover:underline">Neues Regelwerk anlegen</Link> oder per API importieren: POST /api/norms/import
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
|
||||
45
src/app/api/norms/route.ts
Normal file
45
src/app/api/norms/route.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// POST /api/norms — create a new norm instrument (Regelwerk)
|
||||
|
||||
import { db } from "@/lib/db";
|
||||
import { normInstruments } from "@/lib/db/schema";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
let body: Record<string, unknown>;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return Response.json({ error: "Invalid JSON body." }, { status: 400 });
|
||||
}
|
||||
|
||||
const { type, sourceRank, abbreviation, fullTitle, enactedAt, issuingBody, citation } =
|
||||
body as any;
|
||||
|
||||
if (!type || !sourceRank || !abbreviation || !fullTitle) {
|
||||
return Response.json(
|
||||
{ error: "type, sourceRank, abbreviation, and fullTitle are required." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (enactedAt && !/^\d{4}-\d{2}-\d{2}$/.test(enactedAt)) {
|
||||
return Response.json(
|
||||
{ error: "enactedAt must be YYYY-MM-DD." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const [created] = await db
|
||||
.insert(normInstruments)
|
||||
.values({
|
||||
type,
|
||||
sourceRank,
|
||||
abbreviation,
|
||||
fullTitle,
|
||||
enactedAt: enactedAt ?? null,
|
||||
issuingBody: issuingBody ?? null,
|
||||
citation: citation ?? null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return Response.json({ instrument: created }, { status: 201 });
|
||||
}
|
||||
Reference in New Issue
Block a user