feat(stellungnahme): E-Mail, Name in Grußformel, Datum-Fix, UX-Polish
Deploy / verify (push) Successful in 57s
Deploy / deploy (push) Successful in 1m9s

- E-Mail-Button: mailto-Link wenn recipientEmail im Block gesetzt
- Name in Grußformel: "Mit freundlichen Grüßen\n\nMax Mustermann"
- Datum-Fix: heute in buildText() berechnet, nicht beim Render
- Disabled-Hint: roter Hinweis wenn Name fehlt
- "Alle ab" klappt Gruppe ein (Symmetrie zu "Alle" öffnet)
- Schritt X von Y unter Step-Indikator
- recipientEmail-Feld in StellingnahmeGeneratorBlockData

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-05-27 16:32:25 +02:00
parent ff383b8b3b
commit 5f5bd06a78
2 changed files with 35 additions and 36 deletions
+2
View File
@@ -470,5 +470,7 @@ export interface StellingnahmeGeneratorBlockData {
allgemeineArgumente?: TextFragment[]; allgemeineArgumente?: TextFragment[];
/** ISO-Datum: Einwendungsfrist-Ende (z.B. "2026-07-04"). */ /** ISO-Datum: Einwendungsfrist-Ende (z.B. "2026-07-04"). */
deadline?: string; deadline?: string;
/** E-Mail der Planungsstelle — aktiviert Mail-Button. */
recipientEmail?: string;
layout?: BlockLayout; layout?: BlockLayout;
} }
@@ -169,12 +169,11 @@
else next.add(s); else next.add(s);
} }
selectedSlugs = next; selectedSlugs = next;
// Expand group when selecting so user sees what they picked // Expand on select, collapse on deselect
if (!allSelected) { const nextGroups = new Set(expandedGroups);
const nextGroups = new Set(expandedGroups); if (allSelected) nextGroups.delete(group.slug);
nextGroups.add(group.slug); else nextGroups.add(group.slug);
expandedGroups = nextGroups; expandedGroups = nextGroups;
}
} }
// Combined selection for output — ortskonkrete first, then allgemeine // Combined selection for output — ortskonkrete first, then allgemeine
@@ -196,46 +195,35 @@ Regionale Planungsstelle
Karl-Liebknecht-Straße 4 Karl-Liebknecht-Straße 4
98527 Suhl`; 98527 Suhl`;
const heute = $derived(
new Date().toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" }),
);
const gebietsBez = $derived( const gebietsBez = $derived(
windArea windArea
? `${windArea.gebiets_nr}${windArea.bezeichnung ? " " + windArea.bezeichnung : ""}` ? `${windArea.gebiets_nr}${windArea.bezeichnung ? " " + windArea.bezeichnung : ""}`
: "", : "",
); );
const mailtoLink = $derived.by(() => {
if (!block.recipientEmail || !outputText) return "";
const subject = `Einwendung Regionalplan Südwestthüringen Vorranggebiet ${gebietsBez}`;
return `mailto:${block.recipientEmail}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(outputText)}`;
});
function buildText(): string { function buildText(): string {
const heute = new Date().toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" });
const sender = [absenderName.trim(), absenderAnschrift.trim(), absenderOrt.trim()] const sender = [absenderName.trim(), absenderAnschrift.trim(), absenderOrt.trim()]
.filter(Boolean) .filter(Boolean)
.join("\n"); .join("\n");
const betreff = `Einwendung zum Entwurf des Regionalplans Südwestthüringen (2. Entwurf)\nWindenergie-Vorranggebiet ${gebietsBez}`; const betreff = `Einwendung zum Entwurf des Regionalplans Südwestthüringen (2. Entwurf)\nWindenergie-Vorranggebiet ${gebietsBez}`;
let body = ""; const closing = absenderName.trim()
? `Mit freundlichen Grüßen\n\n${absenderName.trim()}`
if (mode === "fertig") { : "Mit freundlichen Grüßen";
body += `ich erhebe Einwendung gegen die Ausweisung des Windenergie-Vorranggebiets ${gebietsBez} im 2. Entwurf des Regionalplans Südwestthüringen.\n\n`; let body = `ich erhebe Einwendung gegen die Ausweisung des Windenergie-Vorranggebiets ${gebietsBez} im 2. Entwurf des Regionalplans Südwestthüringen.\n\n`;
for (const f of selectedFragments) { for (const f of selectedFragments) {
if (f.title) body += `${f.title}\n\n`; if (f.title) body += `${f.title}\n\n`;
if (f.text) body += `${stripMd(f.text)}\n\n`; if (f.text) body += `${stripMd(f.text)}\n\n`;
}
if (persoenlicheEingabe.trim())
body += `Persönliche Betroffenheit\n\n${persoenlicheEingabe.trim()}\n\n`;
body +=
"Ich fordere die Regionalplanung auf, das Vorranggebiet aus dem Regionalplan zu streichen oder zumindest einer erneuten, umfassenden Überprüfung zu unterziehen.\n\nMit freundlichen Grüßen";
} else {
body += `Gliederung meiner Einwendung (bitte in eigenen Worten ausformulieren):\n\n`;
let i = 1;
for (const f of selectedFragments) {
body += `${i}. ${f.title ?? f._slug.replace(/_/g, " ")}\n`;
if (f.bulletPoints?.length)
for (const bp of f.bulletPoints) body += ` - ${bp}\n`;
body += "\n";
i++;
}
if (persoenlicheEingabe.trim())
body += `${i}. Persönliche Betroffenheit\n ${persoenlicheEingabe.trim()}\n\n`;
body += "Ich fordere die Regionalplanung auf, das Vorranggebiet aus dem Regionalplan zu streichen oder zumindest einer erneuten, umfassenden Überprüfung zu unterziehen.\n\nMit freundlichen Grüßen";
} }
if (persoenlicheEingabe.trim())
body += `Persönliche Betroffenheit\n\n${persoenlicheEingabe.trim()}\n\n`;
body += `Ich fordere die Regionalplanung auf, das Vorranggebiet aus dem Regionalplan zu streichen oder zumindest einer erneuten, umfassenden Überprüfung zu unterziehen.\n\n${closing}`;
return [sender, "", heute, "", "An:", RECIPIENT, "", "Betreff: " + betreff.split("\n")[0], betreff.split("\n")[1] ?? "", "", "Sehr geehrte Damen und Herren,", "", body.trimEnd()] return [sender, "", heute, "", "An:", RECIPIENT, "", "Betreff: " + betreff.split("\n")[0], betreff.split("\n")[1] ?? "", "", "Sehr geehrte Damen und Herren,", "", body.trimEnd()]
.join("\n") .join("\n")
@@ -473,7 +461,7 @@ Karl-Liebknecht-Straße 4
<div class="prose prose-wald mb-6 max-w-none text-stein-700">{@html md(block.intro)}</div> <div class="prose prose-wald mb-6 max-w-none text-stein-700">{@html md(block.intro)}</div>
{/if} {/if}
<!-- Step indicator --> <!-- Step indicator -->
<div class="mb-2 flex items-center gap-1 text-xs font-medium"> <div class="mb-2 flex items-center gap-1 text-xs font-medium">
{#each Array.from({ length: TOTAL_STEPS }, (_, i) => i) as s} {#each Array.from({ length: TOTAL_STEPS }, (_, i) => i) as s}
<button <button
@@ -492,6 +480,7 @@ Karl-Liebknecht-Straße 4
<div class="h-px flex-1 {s < step ? 'bg-wald-400' : 'bg-stein-200'}"></div> <div class="h-px flex-1 {s < step ? 'bg-wald-400' : 'bg-stein-200'}"></div>
{/if} {/if}
{/each} {/each}
</div>
<p class="mb-6 text-right text-xs text-stein-400">Schritt {step + 1} von {TOTAL_STEPS} {STEP_LABELS[step]}</p> <p class="mb-6 text-right text-xs text-stein-400">Schritt {step + 1} von {TOTAL_STEPS} {STEP_LABELS[step]}</p>
<!-- ── Step 0: Gebietskontext ─────────────────────────────────────────────── --> <!-- ── Step 0: Gebietskontext ─────────────────────────────────────────────── -->
@@ -811,7 +800,7 @@ Karl-Liebknecht-Straße 4
<div class="mb-1 font-semibold text-stein-700">Einzureichen bei:</div> <div class="mb-1 font-semibold text-stein-700">Einzureichen bei:</div>
<div class="whitespace-pre">{RECIPIENT}</div> <div class="whitespace-pre">{RECIPIENT}</div>
</div> </div>
<div class="mb-2 flex gap-3"> <div class="mb-2 flex gap-3">
<button <button
class="rounded-lg bg-wald-700 px-6 py-2.5 font-semibold text-white transition hover:bg-wald-800 disabled:opacity-40" class="rounded-lg bg-wald-700 px-6 py-2.5 font-semibold text-white transition hover:bg-wald-800 disabled:opacity-40"
@@ -819,6 +808,11 @@ Karl-Liebknecht-Straße 4
onclick={generateOutput} onclick={generateOutput}
>Text erstellen</button> >Text erstellen</button>
<button class="rounded-lg border border-stein-300 px-4 py-2 text-sm text-stein-600 hover:bg-stein-50" onclick={() => goTo(3)}> Zurück</button> <button class="rounded-lg border border-stein-300 px-4 py-2 text-sm text-stein-600 hover:bg-stein-50" onclick={() => goTo(3)}> Zurück</button>
</div>
{#if !absenderName.trim()}
<p class="mb-4 text-xs text-erde-600">Bitte Namen eintragen, um den Text zu erstellen.</p>
{:else}
<div class="mb-4"></div>
{/if} {/if}
{#if showOutput} {#if showOutput}
@@ -828,6 +822,9 @@ Karl-Liebknecht-Straße 4
<div class="flex gap-2"> <div class="flex gap-2">
<button class="rounded-md border border-stein-300 px-3 py-1.5 text-xs font-medium text-stein-700 hover:bg-stein-50" onclick={copyText}>{copyFeedback || "Kopieren"}</button> <button class="rounded-md border border-stein-300 px-3 py-1.5 text-xs font-medium text-stein-700 hover:bg-stein-50" onclick={copyText}>{copyFeedback || "Kopieren"}</button>
<button class="rounded-md border border-stein-300 px-3 py-1.5 text-xs font-medium text-stein-700 hover:bg-stein-50" onclick={downloadText}>Herunterladen</button> <button class="rounded-md border border-stein-300 px-3 py-1.5 text-xs font-medium text-stein-700 hover:bg-stein-50" onclick={downloadText}>Herunterladen</button>
<button class="rounded-md border border-stein-300 px-3 py-1.5 text-xs font-medium text-stein-700 hover:bg-stein-50" onclick={printText}>Drucken</button>
{#if mailtoLink}
<a href={mailtoLink} class="rounded-md border border-wald-400 bg-wald-50 px-3 py-1.5 text-xs font-medium text-wald-700 hover:bg-wald-100">Per E-Mail senden</a>
{/if} {/if}
</div> </div>
</div> </div>