Compare commits

..

No commits in common. "ecaf5ab897801d37e3e8831281e40e6c6c1cff0b" and "8ae984060115951343fdc9b8fbb172150fc80159" have entirely different histories.

9 changed files with 821 additions and 954 deletions

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1772047000,
"narHash": "sha256-7DaQVv4R97cii/Qdfy4tmDZMB2xxtyIvNGSwXBBhSmo=",
"lastModified": 1766473571,
"narHash": "sha256-5G1NDO2PulBx1RoaA6U1YoUDX0qZslpPxv+n5GX6Qto=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1267bb4920d0fc06ea916734c11b0bf004bbe17e",
"rev": "76701a179d3a98b07653e2b0409847499b2a07d3",
"type": "github"
},
"original": {

View file

@ -18,7 +18,7 @@
overlay = final: prev: {
meinantrag = with final; python3Packages.buildPythonApplication rec {
pname = "meinantrag";
version = "0.0.3";
version = "0.0.2";
format = "other";
src = self;
@ -30,7 +30,7 @@
requests
jinja2
google-generativeai # Dependency for Gemini API
grpcio # Required by google-generativeai
reportlab # Dependency for PDF generation
python-docx # Dependency for Word document generation
];

File diff suppressed because it is too large Load diff

View file

@ -16,31 +16,16 @@ in
enable = lib.mkEnableOption "MeinAntrag web app";
settings = lib.mkOption {
environment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = {
GOOGLE_GEMINI_API_KEY = "your-api-key-here";
MEINANTRAG_BASE_URL = "https://example.com";
GOOGLE_GEMINI_API_KEY = "file:/run/secrets/gemini_api_key";
};
description = ''
Additional environment variables to pass to the MeinAntrag service.
Values starting with "file:" will be read from the specified path.
For secrets with systemd LoadCredential, use the credentials option instead.
'';
};
credentials = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = {
GOOGLE_GEMINI_API_KEY = "/run/secrets/gemini_api_key";
};
description = ''
Credentials to pass to the MeinAntrag service.
Maps environment variable names to file paths containing the secret values.
These are loaded via systemd's LoadCredential mechanism.
The Python app will automatically read the value from the file.
For example, set GOOGLE_GEMINI_API_KEY for Gemini API integration.
'';
};
@ -79,8 +64,7 @@ in
"PYTHONPATH=${pkgs.meinantrag}/share/meinantrag:${pkgs.meinantrag.pythonPath}"
"MEINANTRAG_TEMPLATES_DIR=${pkgs.meinantrag}/share/meinantrag/templates"
"MEINANTRAG_STATIC_DIR=${pkgs.meinantrag}/share/meinantrag/assets"
] ++ (lib.mapAttrsToList (name: value: "${name}=${value}") cfg.settings)
++ (lib.mapAttrsToList (name: _: "${name}=file:/run/credentials/uwsgi.service/${name}") cfg.credentials);
] ++ (lib.mapAttrsToList (name: value: "${name}=${value}") cfg.environment);
settings = {
"static-map" = "/static=${pkgs.meinantrag}/share/meinantrag/assets";
@ -90,10 +74,6 @@ in
};
};
# Load credentials via systemd's LoadCredential mechanism
systemd.services.uwsgi.serviceConfig.LoadCredential =
lib.mapAttrsToList (key: value: "${key}:${value}") cfg.credentials;
# Ensure meinantrag user and group exist
users.users.meinantrag = {
isSystemUser = true;

View file

@ -4,24 +4,24 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}MeinAntrag{% endblock %}</title>
<link type="text/css" href="/static/css/pages.css" media="all" rel="stylesheet">
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/select2.min.css" rel="stylesheet">
<link href="/static/css/select2-bootstrap-5-theme.min.css" rel="stylesheet">
<meta name="description" content="{{ meta_description | default('Erstelle einfach Vorlagen für Anfragen oder Anträge an die Karlsruher Stadtverwaltung zu deinem persönlichen Thema und schicke diese direkt an eine Stadtratsfraktion!') }}">
<meta name="description" content="{{ meta_description | default('Erstelle vorausgefüllte FragDenStaat.de-Anfragelinks und teile sie mit Freund:innen. Suche Behörden, füge Betreff und Text hinzu und generiere einen teilbaren Link.') }}">
<link rel="canonical" href="{{ canonical_url | default('/') }}">
<link rel="alternate" hreflang="de" href="{{ canonical_url | default('/') }}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="MeinAntrag">
<meta property="og:title" content="{{ meta_title | default('MeinAntrag') }}">
<meta property="og:description" content="{{ meta_description | default('Erstelle einfach Vorlagen für Anfragen oder Anträge an die Karlsruher Stadtverwaltung zu deinem persönlichen Thema und schicke diese direkt an eine Stadtratsfraktion!') }}">
<meta property="og:description" content="{{ meta_description | default('Erstelle vorausgefüllte FragDenStaat.de-Anfragelinks und teile sie mit Freund:innen.') }}">
<meta property="og:locale" content="de_DE">
<meta property="og:url" content="{{ canonical_url | default('/') }}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ meta_title | default('MeinAntrag') }}">
<meta name="twitter:description" content="{{ meta_description | default('Erstelle einfach Vorlagen für Anfragen oder Anträge an die Karlsruher Stadtverwaltung zu deinem persönlichen Thema und schicke diese direkt an eine Stadtratsfraktion!') }}">
<meta name="twitter:description" content="{{ meta_description | default('Erstelle vorausgefüllte FragDenStaat.de-Anfragelinks und teile sie mit Freund:innen.') }}">
{% if noindex %}<meta name="robots" content="noindex,follow">{% endif %}
<meta name="theme-color" content="#667eea">
@ -30,211 +30,61 @@
{% block meta_extra %}{% endblock %}
<style>
h2 {
max-width: 700px !important;
}
select, textarea, input {
max-width: 700px;
margin-bottom: 1rem;
}
.tutorial-list li {
margin-bottom: 30px;
}
.tutorial-list li h4 {
margin-bottom: 10px;
}
.tutorial-list li p {
margin: 10px 0;
}
/* Responsive design for mobile devices */
@media (max-width: 768px) {
h2 {
max-width: 100% !important;
font-size: 1.2rem;
padding: 0 1rem;
}
select, textarea, input {
max-width: 100%;
width: 100%;
margin-bottom: 1rem;
}
.tutorial-list li {
padding: 1rem;
}
.tutorial-list li h4 {
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
.tutorial-list li p {
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.form-control, .form-select {
font-size: 16px; /* Prevents zoom on iOS */
padding: 0.75rem;
}
textarea.form-control {
min-height: 150px;
}
.btn-primary {
width: 100%;
padding: 0.75rem;
font-size: 16px;
margin-bottom: 0.5rem;
box-sizing: border-box;
}
#resultFields .btn-primary {
margin-right: 0;
margin-left: 0;
display: block;
width: 100%;
margin-bottom: 0.5rem;
}
#resultFields .btn-primary:last-child {
margin-bottom: 0;
}
.tutorial-list li form {
width: 100%;
}
.tutorial-list li form > div {
width: 100%;
}
/* Fix wrapper width on mobile */
.wrapper {
width: 100% !important;
max-width: 100%;
}
.page-footer {
width: 100% !important;
padding: 20px 1rem;
margin: 40px auto 0;
position: relative;
}
.page-footer .site-footer-links {
display: block;
width: 100%;
text-align: center;
margin: 0.5rem 0;
padding: 0;
font-size: 0.85rem;
}
.page-footer .site-footer-links.right {
float: none;
text-align: center;
margin-bottom: 1rem;
}
.page-footer .site-footer-links li {
display: inline-block;
margin: 0 0.5rem;
vertical-align: middle;
}
.tutorial-list {
margin-top: 40px;
}
.tutorial-list p {
margin: 15px 20px !important;
}
.hero-spot {
padding: 2rem 1rem;
}
.hero-spot h1 {
font-size: 2rem;
margin-top: 1rem;
}
.hero-spot h2 {
font-size: 1rem;
padding: 0 1rem;
margin-bottom: 2rem;
}
.tutorial {
padding: 1rem 0;
}
.wrapper {
padding: 0 1rem;
max-width: 100%;
}
.tutorial-list li {
margin-bottom: 2rem;
padding: 1.5rem 1rem;
}
.tutorial-list li:after {
left: 50%;
transform: translateX(-50%);
top: -29px;
}
}
@media (max-width: 480px) {
.hero-spot h1 {
font-size: 1.5rem;
}
.hero-spot h2 {
font-size: 0.9rem;
}
.tutorial-list li h4 {
font-size: 1rem;
}
.page-footer .site-footer-links {
display: block;
margin: 0.5rem 0;
text-align: center;
}
.page-footer .site-footer-links.right {
float: none;
text-align: center;
}
.page-footer .site-footer-links li {
display: block;
margin: 0.5rem 0;
}
.tutorial-list li {
padding: 1rem 0.5rem;
}
.tutorial-list li h4 {
font-size: 0.95rem;
}
.tutorial-list li p {
font-size: 0.85rem;
margin: 0.5rem 0;
}
}
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.main-container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
padding: 3rem;
margin: 2rem auto;
max-width: 800px;
}
.title {
color: #2c3e50;
font-weight: 700;
margin-bottom: 1rem;
}
.description {
color: #7f8c8d;
font-size: 1.1rem;
margin-bottom: 2.5rem;
line-height: 1.6;
}
.form-control, .form-select {
border-radius: 10px;
border: 2px solid #e9ecef;
padding: 0.75rem 1rem;
transition: all 0.3s ease;
}
.form-control:focus, .form-select:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
.btn-primary {
background: linear-gradient(45deg, #667eea, #764ba2);
border: none;
border-radius: 15px;
padding: 1rem 2rem;
font-size: 1.1rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.result-link {
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 10px;
padding: 1rem;
margin-top: 1rem;
word-break: break-all;
}
/* Ensure Select2 scales properly */
.select2-container {
width: 100% !important;
@ -250,58 +100,52 @@
.select2-container .select2-selection__arrow {
height: 100% !important;
}
.loading {
display: none;
}
.form-control, .form-select {
width: 100%;
padding: 0.5rem;
margin-top: 0.5rem;
border: 1px solid #ddd;
border-radius: 3px;
font-size: 14px;
}
.form-control:focus, .form-select:focus {
outline: none;
border-color: #4078c0;
box-shadow: 0 0 0 2px rgba(64, 120, 192, 0.2);
}
textarea.form-control {
min-height: 200px;
resize: vertical;
}
.btn-primary {
background-color: #4078c0;
border-color: #4078c0;
color: white;
padding: 0.5rem 1rem;
border-radius: 3px;
text-decoration: none;
display: inline-block;
border: none;
cursor: pointer;
font-size: 14px;
}
.btn-primary:hover {
background-color: #3669a3;
border-color: #3669a3;
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
/* Mobile adjustments */
@media (max-width: 576px) {
.main-container {
padding: 1.25rem;
margin: 1rem auto 4rem auto;
}
.title {
font-size: 2rem;
}
.description {
font-size: 1rem;
}
}
.loading {
display: none;
}
.loading.active {
display: inline-block;
margin-left: 0.5rem;
.footer {
background: transparent;
margin-top: 3rem;
}
.footer-links a {
font-size: 0.9rem;
transition: color 0.3s ease;
}
.footer-links a:hover {
color: #667eea !important;
}
.footer-text {
font-size: 0.8rem;
}
.footer-text a:hover {
color: #667eea !important;
}
.footer-text a {
text-decoration: underline;
}
.footer-text {
font-size: 0.8rem;
margin-bottom: 2rem;
}
.legal-content {
text-align: left;
line-height: 1.6;
}
.legal-content h2 {
color: #2c3e50;
margin-top: 2rem;
margin-bottom: 1rem;
}
@ -311,25 +155,42 @@
.legal-content ul {
margin-bottom: 1rem;
}
.main-container {
margin-bottom: 4rem;
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body class="home">
{% block content %}{% endblock %}
<body>
<div class="container">
<div class="main-container">
{% block content %}{% endblock %}
</div>
<footer class="page-footer">
<ul class="site-footer-links right">
<li><a href="/impressum">Impressum</a></li>
<li><a href="/datenschutz">Datenschutz</a></li>
<li><a href="https://git.project-insanity.org/onny/MeinAntrag" target="_blank">Source</a></li>
</ul>
<ul class="site-footer-links">
<li>&copy; 2025 <span>Project-Insanity.org</span></li>
<li><a href="https://social.project-insanity.org/@pi_crew" target="_blank">Mastodon</a></li>
</ul>
</footer>
<!-- Footer -->
<footer class="footer mt-5 pt-4">
<div class="container">
<div class="row">
<div class="col-12 text-center">
<div class="footer-links mb-2">
<a href="/impressum" class="text-muted text-decoration-none me-3">Impressum</a>
<a href="/datenschutz" class="text-muted text-decoration-none me-3">Datenschutz</a>
<a href="https://git.project-insanity.org/onny/MeinAntrag" class="text-muted text-decoration-none" target="_blank">Source</a>
</div>
<div class="footer-text">
<small class="text-muted">
Projekt von <a href="https://project-insanity.org" class="text-muted text-decoration-none" target="_blank">Project-Insanity.org</a>,
follow us on <a href="https://social.project-insanity.org/@pi_crew" class="text-muted text-decoration-none" target="_blank">Mastodon</a> :)
</small>
</div>
</div>
</div>
</div>
</footer>
</div>
<script src="/static/js/pages-jquery.js"></script>
<script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/select2.min.js"></script>
{% block extra_js %}{% endblock %}

View file

@ -3,90 +3,90 @@
{% block title %}MeinAntrag{% endblock %}
{% block content %}
<section id="hero-spot" class="hero-spot">
<h1>MeinAntrag</h1>
<h2>Erstelle einfach Vorlagen für Anfragen oder Anträge an die Karlsruher Stadtverwaltung zu deinem persönlichen Thema und schicke diese direkt an eine Stadtratsfraktion!</h2>
</section>
<div class="text-center">
<h1 class="title display-4">MeinAntrag</h1>
<p class="description">
Erstelle einfach Vorlagen für Anfragen oder Anträge an die Karlsruher Stadtverwaltung
zu deinem persönlichen Thema und schicke diese direkt an eine Stadtratsfraktion!
</p>
<section id="tutorial" class="tutorial">
<ul id="project-site" class="tutorial-list wrapper active">
<li class="question">
<form id="meinantragForm">
<div id="inputFields">
<li>
<h4>Fraktion auswählen</h4>
<p>Wähle die Fraktion, an die der Antrag gerichtet ist:</p>
<select class="form-select" id="party" name="party_id" required>
<option value="">Fraktion auswählen...</option>
<option value="SPD">SPD</option>
<option value="GRÜNEN">GRÜNEN</option>
<option value="CDU">CDU</option>
<option value="FDP/FW">FDP/FW</option>
<option value="Volt">Volt</option>
<option value="DIE LINKE">DIE LINKE</option>
<option value="KAL">KAL</option>
</select>
</li>
<li>
<h4>Dein Anliegen beschreiben</h4>
<p>Beschreibe hier, welche Anfrage oder Antrag du an die Stadtverwaltung stellen möchtest:</p>
<textarea class="form-control" id="anliegen" name="anliegen" rows="5" required></textarea>
</li>
<li>
<button type="submit" class="btn btn-primary">
<span class="btn-text">Antrag generieren</span>
<span class="loading" role="status" style="display: none;">...</span>
</button>
</li>
<form id="meinantragForm" class="text-start">
<div id="inputFields">
<div class="mb-4">
<label for="party" class="form-label fw-bold">Fraktion</label>
<select class="form-select" id="party" name="party_id" required>
<option value="">Fraktion auswählen...</option>
<option value="SPD">SPD</option>
<option value="GRÜNEN">GRÜNEN</option>
<option value="CDU">CDU</option>
<option value="FDP/FW">FDP/FW</option>
<option value="Volt">Volt</option>
<option value="DIE LINKE">DIE LINKE</option>
<option value="KAL">KAL</option>
</select>
</div>
<div id="resultFields" style="display: none;">
<li>
<p><a href="#" id="backLink">← Zurück zum Formular</a></p>
<br><br>
</li>
<li>
<h4>Antragstitel</h4>
<p></p>
<input type="text" class="form-control" id="antragstitel" name="antragstitel">
</li>
<li>
<h4>Forderung</h4>
<p></p>
<textarea class="form-control" id="forderung" name="forderung" rows="5"></textarea>
</li>
<li>
<h4>Begründung/Sachverhalt</h4>
<p></p>
<textarea class="form-control" id="begruendung" name="begruendung" rows="8"></textarea>
</li>
<li>
<h4>Aktionen</h4>
<p>Du kannst den Antrag jetzt per E-Mail senden oder als Word-Datei herunterladen:</p>
<button type="button" class="btn btn-primary" id="mailBtn">
<span id="mailBtnText">Mail an Fraktion senden</span>
</button>
<button type="button" class="btn btn-primary" id="wordBtn">
Word-Datei herunterladen
</button>
</li>
<div class="mb-4">
<label for="anliegen" class="form-label fw-bold">Mein Anliegen:</label>
<textarea class="form-control" id="anliegen" name="anliegen" rows="5"
placeholder="Beschreibe hier, welche Anfrage oder Antrag du an die Stadtverwaltung stellen möchtest ..." required></textarea>
</div>
</form>
<div class="text-center">
<button type="submit" class="btn btn-primary btn-lg">
<span class="btn-text">Antrag generieren</span>
<span class="loading spinner-border spinner-border-sm ms-2" role="status"></span>
</button>
</div>
</div>
</p>
</li>
</ul>
<div id="resultFields" class="text-start" style="display: none;">
<div class="mb-3">
<a href="#" id="backLink" class="text-decoration-none">
<span>← Zurück</span>
</a>
</div>
</section>
<div class="mb-4">
<label for="antragstitel" class="form-label fw-bold">Antragstitel</label>
<input type="text" class="form-control" id="antragstitel" name="antragstitel">
</div>
<div class="mb-4">
<label for="forderung" class="form-label fw-bold">Forderung</label>
<textarea class="form-control" id="forderung" name="forderung" rows="5"></textarea>
</div>
<div class="mb-4">
<label for="begruendung" class="form-label fw-bold">Begründung/Sachverhalt</label>
<textarea class="form-control" id="begruendung" name="begruendung" rows="8"></textarea>
</div>
<div class="mb-4 d-flex gap-2 flex-wrap justify-content-center">
<button type="button" class="btn btn-primary" id="mailBtn">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-envelope me-2" viewBox="0 0 16 16">
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1zm13 2.383-4.708 2.825L15 11.105zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741M1 11.105l4.708-2.897L1 5.383z"/>
</svg>
<span id="mailBtnText">Mail an Fraktion senden</span>
</button>
<button type="button" class="btn btn-primary" id="pdfBtn">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-pdf me-2" viewBox="0 0 16 16">
<path d="M8.5 6a.5.5 0 0 0-1 0v1.5H6a.5.5 0 0 0 0 1h1.5V10a.5.5 0 0 0 1 0V8.5H10a.5.5 0 0 0 0-1H8.5z"/>
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
</svg>
PDF anzeigen
</button>
<button type="button" class="btn btn-primary" id="wordBtn">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-word me-2" viewBox="0 0 16 16">
<path d="M5.485 6.879a.5.5 0 1 0-.97.242l1.5 6a.5.5 0 0 0 .539.314l1.5-.5a.5.5 0 0 0 .186-.596l-.737-2.945 2.679-3.42a.5.5 0 1 0-.758-.652L6.978 8.616l-1.493-.5z"/>
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
</svg>
Word-Datei herunterladen
</button>
</div>
</div>
</form>
</div>
{% endblock %}
{% block extra_js %}
@ -109,8 +109,8 @@
const $btnText = $button.find('.btn-text');
const $loading = $button.find('.loading');
$button.prop('disabled', true);
$btnText.text('Generiere Antrag');
$loading.css('display', 'inline');
$btnText.text('Generiere Antrag...');
$loading.css('display', 'inline-block');
// Prepare form data as URL-encoded
const formData = new URLSearchParams();
@ -142,9 +142,8 @@
$('#forderung').val(data.demand || '');
$('#begruendung').val(data.justification || '');
// Store party name and email body for mail button
// Store party name for mail button
$('#resultFields').data('party-name', data.party_name || '');
$('#resultFields').data('email-body', data.email_body || '');
// Update mail button text
if (data.party_name) {
@ -161,7 +160,7 @@
// Reset button state
$button.prop('disabled', false);
$btnText.text('Antrag generieren');
$loading.css('display', 'none');
$loading.hide();
})
.catch(error => {
console.error('Error:', error);
@ -170,7 +169,7 @@
// Reset button state
$button.prop('disabled', false);
$btnText.text('Antrag generieren');
$loading.css('display', 'none');
$loading.hide();
});
});
@ -183,13 +182,13 @@
// Mail-Kontakte für Fraktionen
const partyContacts = {
'SPD': 'spd@fraktion.karlsruhe.de',
'GRÜNEN': 'gruene@fraktion.karlsruhe.de',
'CDU': 'cdu@fraktion.karlsruhe.de',
'FDP/FW': 'fdp-fw@fraktion.karlsruhe.de',
'Volt': 'volt@fraktion.karlsruhe.de',
'DIE LINKE': 'dielinke@gr.karlsruhe.de',
'KAL': 'kal@fraktion.karlsruhe.de'
'SPD': 'spd@karlsruhe.de',
'GRÜNEN': 'gruene@karlsruhe.de',
'CDU': 'cdu@karlsruhe.de',
'FDP/FW': 'fdp@karlsruhe.de',
'Volt': 'volt@karlsruhe.de',
'DIE LINKE': 'dielinke@karlsruhe.de',
'KAL': 'kal@karlsruhe.de'
};
// Handle mail button click
@ -197,9 +196,18 @@
const partyName = $('#resultFields').data('party-name') || '';
const email = partyContacts[partyName] || '';
const subject = encodeURIComponent($('#antragstitel').val() || '');
const emailBody = $('#resultFields').data('email-body') || '';
const title = $('#antragstitel').val() || '';
const demand = $('#forderung').val() || '';
const justification = $('#begruendung').val() || '';
const bodyEncoded = encodeURIComponent(emailBody);
// Build email body
let body = title + '\n\n';
body += 'Der Gemeinderat möge beschließen:\n';
body += demand + '\n\n';
body += 'Begründung/Sachverhalt\n';
body += justification;
const bodyEncoded = encodeURIComponent(body);
// Open mail client
if (email) {
@ -209,37 +217,45 @@
}
});
// Function to create a valid filename from title
function createFilename(title) {
if (!title || !title.trim()) {
return 'antrag.docx';
// Handle PDF button click
$('#pdfBtn').on('click', function() {
const title = $('#antragstitel').val() || '';
const demand = $('#forderung').val() || '';
const justification = $('#begruendung').val() || '';
const partyName = $('#resultFields').data('party-name') || '';
// Prepare form data
const formData = new URLSearchParams();
formData.append('title', title);
formData.append('demand', demand);
formData.append('justification', justification);
if (partyName) {
formData.append('party_name', partyName);
}
// Remove or replace special characters
let filename = title
.trim()
.toLowerCase()
.replace(/[ä]/g, 'ae')
.replace(/[ö]/g, 'oe')
.replace(/[ü]/g, 'ue')
.replace(/[ß]/g, 'ss')
.replace(/[^a-z0-9\s-]/g, '') // Remove special chars except spaces and hyphens
.replace(/\s+/g, '_') // Replace spaces with underscores
.replace(/_+/g, '_') // Replace multiple underscores with single
.replace(/^_+|_+$/g, ''); // Remove leading/trailing underscores
// Limit length to 100 characters
if (filename.length > 100) {
filename = filename.substring(0, 100);
}
// If empty after cleaning, use default
if (!filename) {
return 'antrag.docx';
}
return filename + '.docx';
}
// Open PDF in new window
fetch('/api/generate-pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData.toString()
})
.then(response => {
if (!response.ok) {
throw new Error('Fehler beim Generieren des PDFs');
}
return response.blob();
})
.then(blob => {
const url = window.URL.createObjectURL(blob);
window.open(url, '_blank');
})
.catch(error => {
console.error('Error:', error);
alert('Fehler beim Generieren des PDFs: ' + error.message);
});
});
// Handle Word button click
$('#wordBtn').on('click', function() {
@ -257,9 +273,6 @@
formData.append('party_name', partyName);
}
// Generate filename from title
const filename = createFilename(title);
// Download Word file
fetch('/api/generate-word', {
method: 'POST',
@ -278,7 +291,7 @@
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.download = 'antrag.docx';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);