contentpanel - Aushangworkflow um docx Vorlage erweitern
This commit is contained in:
@@ -1,321 +1,275 @@
|
||||
/* Schwarzes Brett / Contentpanel */
|
||||
.sb-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: calc(var(--header-height) + var(--header-title-height));
|
||||
width: 100%;
|
||||
min-height: calc(100vh - var(--header-height) - var(--header-title-height) - var(--footer-height));
|
||||
padding: var(--space-lg) var(--space-md) var(--space-3xl);
|
||||
background: linear-gradient(135deg, var(--gray-50) 0%, hsl(330, 60%, 95%) 100%);
|
||||
}
|
||||
|
||||
.sb-page > .content-tab-bar {
|
||||
margin: calc(-1 * var(--space-lg)) calc(-1 * var(--space-md)) 0;
|
||||
width: calc(100% + 2 * var(--space-md));
|
||||
}
|
||||
|
||||
.sb-tab-content {
|
||||
display: none;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin: var(--space-lg) auto 0;
|
||||
animation: sbFadeIn 300ms ease;
|
||||
}
|
||||
|
||||
.sb-tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes sbFadeIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.sb-card {
|
||||
margin-bottom: var(--space-lg);
|
||||
padding: 24px 28px;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.sb-card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 var(--space-md);
|
||||
color: var(--gray-800);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sb-card-title svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
fill: var(--nak-raspberry);
|
||||
}
|
||||
|
||||
.sb-entry-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 360px;
|
||||
margin-bottom: var(--space-lg);
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.sb-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--gray-100);
|
||||
}
|
||||
|
||||
.sb-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sb-entry-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sb-entry-date {
|
||||
color: var(--nak-raspberry);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sb-entry-title {
|
||||
margin-top: 2px;
|
||||
color: var(--gray-800);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sb-entry-subtitle,
|
||||
.sb-entry-meta {
|
||||
margin-top: 2px;
|
||||
color: var(--gray-500);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.sb-entry-subtitle {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.sb-empty {
|
||||
padding: 16px;
|
||||
color: var(--gray-500);
|
||||
font-size: var(--font-size-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sb-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.sb-form-grid-single {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sb-form-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.sb-field-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.sb-field-group-action {
|
||||
margin-top: var(--space-sm);
|
||||
}
|
||||
|
||||
.sb-label {
|
||||
color: var(--gray-700);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sb-required {
|
||||
color: var(--nak-red);
|
||||
}
|
||||
|
||||
.sb-optional {
|
||||
color: var(--gray-500);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.sb-field {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--gray-300);
|
||||
outline: none;
|
||||
background: var(--gray-50);
|
||||
font-family: var(--font-primary);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 300;
|
||||
transition: border-color 150ms ease, box-shadow 150ms ease;
|
||||
}
|
||||
|
||||
.sb-field:focus {
|
||||
border-color: var(--nak-raspberry);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 3px rgba(215, 50, 130, 0.15);
|
||||
}
|
||||
|
||||
.sb-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-height: 40px;
|
||||
padding: 10px 20px;
|
||||
border: 1px solid var(--gray-300);
|
||||
background: #ffffff;
|
||||
color: var(--gray-700);
|
||||
cursor: pointer;
|
||||
font-family: var(--font-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition: background 150ms ease, transform 150ms ease, box-shadow 150ms ease;
|
||||
}
|
||||
|
||||
.sb-btn:hover {
|
||||
background: var(--gray-100);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.sb-btn-primary {
|
||||
border-color: var(--nak-raspberry);
|
||||
background: linear-gradient(135deg, var(--nak-raspberry), #b8296e);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.sb-btn-primary:hover {
|
||||
border-color: #b8296e;
|
||||
background: linear-gradient(135deg, #b8296e, #9c2260);
|
||||
}
|
||||
|
||||
.sb-btn-danger {
|
||||
min-height: 30px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--gray-300);
|
||||
background: none;
|
||||
color: var(--nak-red);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.sb-btn-danger:hover {
|
||||
border-color: var(--nak-red);
|
||||
background: hsl(0, 78%, 96%);
|
||||
}
|
||||
|
||||
.sb-alert {
|
||||
margin: var(--space-md) 0;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid hsl(145, 50%, 85%);
|
||||
background: hsl(145, 50%, 93%);
|
||||
color: hsl(145, 50%, 28%);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sb-alert-error {
|
||||
border-color: hsl(0, 70%, 86%);
|
||||
background: hsl(0, 78%, 96%);
|
||||
color: hsl(0, 65%, 38%);
|
||||
}
|
||||
|
||||
.sb-aushang-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: var(--space-md);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.sb-aushang-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--gray-200);
|
||||
background: var(--gray-50);
|
||||
transition: box-shadow 200ms ease, transform 200ms ease;
|
||||
}
|
||||
|
||||
.sb-aushang-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.sb-aushang-card img {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.sb-aushang-title {
|
||||
color: var(--gray-800);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sb-aushang-meta {
|
||||
color: var(--gray-500);
|
||||
font-size: var(--font-size-xs);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sb-aushang-delete-form {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sb-spinner {
|
||||
display: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0 auto;
|
||||
border: 3px solid var(--gray-200);
|
||||
border-top-color: var(--nak-raspberry);
|
||||
border-radius: 50%;
|
||||
animation: sbSpin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.sb-uploading .sb-upload-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sb-uploading .sb-spinner {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes sbSpin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sb-page { padding: var(--space-md) var(--space-sm); }
|
||||
.sb-card { padding: 16px; }
|
||||
.sb-form-grid { grid-template-columns: 1fr; }
|
||||
.sb-aushang-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
|
||||
}
|
||||
/* Schwarzes Brett / Contentpanel */
|
||||
.sb-page, .sb-entry-list, .sb-field-group, .sb-aushang-card { display: flex; flex-direction: column; }
|
||||
|
||||
.sb-page, .sb-aushang-card { align-items: center; }
|
||||
|
||||
.sb-card-title, .sb-entry { display: flex; align-items: center; }
|
||||
|
||||
.sb-card-title, .sb-entry-title, .sb-aushang-title { color: var(--gray-800); font-weight: 600; }
|
||||
|
||||
.sb-entry-title, .sb-aushang-title, .sb-empty, .sb-label, .sb-btn, .sb-alert { font-size: var(--font-size-sm); }
|
||||
|
||||
.sb-entry-subtitle, .sb-entry-meta, .sb-aushang-meta { color: var(--gray-500); font-size: var(--font-size-xs); }
|
||||
|
||||
.sb-field, .sb-btn { font-family: var(--font-primary); }
|
||||
|
||||
.sb-aushang-title, .sb-aushang-meta, .sb-aushang-delete-form { text-align: center; }
|
||||
|
||||
.sb-page {
|
||||
position: relative;
|
||||
margin-top: calc(var(--header-height) + var(--header-title-height));
|
||||
width: 100%;
|
||||
min-height: calc(100vh - var(--header-height) - var(--header-title-height) - var(--footer-height));
|
||||
padding: var(--space-lg) var(--space-md) var(--space-3xl);
|
||||
background: linear-gradient(135deg, var(--gray-50) 0%, hsl(330, 60%, 95%) 100%);
|
||||
}
|
||||
|
||||
.sb-page > .content-tab-bar { margin: calc(-1 * var(--space-lg)) calc(-1 * var(--space-md)) 0; width: calc(100% + 2 * var(--space-md)); }
|
||||
|
||||
.sb-tab-content {
|
||||
display: none;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin: var(--space-lg) auto 0;
|
||||
animation: sbFadeIn 300ms ease;
|
||||
}
|
||||
|
||||
.sb-tab-content.active { display: block; }
|
||||
|
||||
@keyframes sbFadeIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.sb-card {
|
||||
margin-bottom: var(--space-lg);
|
||||
padding: 24px 28px;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.sb-card-title {
|
||||
gap: 8px;
|
||||
margin: 0 0 var(--space-md);
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sb-card-title svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
fill: var(--nak-raspberry);
|
||||
}
|
||||
|
||||
.sb-entry-list {
|
||||
max-height: 360px;
|
||||
margin-bottom: var(--space-lg);
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.sb-entry {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--gray-100);
|
||||
}
|
||||
|
||||
.sb-entry:last-child { border-bottom: none; }
|
||||
|
||||
.sb-entry-info { flex: 1; min-width: 0; }
|
||||
|
||||
.sb-entry-date {
|
||||
color: var(--nak-raspberry);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sb-entry-title { margin-top: 2px; }
|
||||
|
||||
.sb-entry-subtitle, .sb-entry-meta { margin-top: 2px; }
|
||||
|
||||
.sb-entry-subtitle { font-style: italic; }
|
||||
|
||||
.sb-empty {
|
||||
padding: 16px;
|
||||
color: var(--gray-500);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sb-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.sb-form-grid-single { grid-template-columns: 1fr; }
|
||||
|
||||
.sb-form-full { grid-column: 1 / -1; }
|
||||
|
||||
.sb-field-group { gap: 4px; }
|
||||
|
||||
.sb-field-group-action { margin-top: var(--space-sm); }
|
||||
|
||||
.sb-label { color: var(--gray-700); font-weight: 500; }
|
||||
|
||||
.sb-required { color: var(--nak-red); }
|
||||
|
||||
.sb-optional { color: var(--gray-500); font-weight: 300; }
|
||||
|
||||
.sb-field {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--gray-300);
|
||||
outline: none;
|
||||
background: var(--gray-50);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 300;
|
||||
transition: border-color 150ms ease, box-shadow 150ms ease;
|
||||
}
|
||||
|
||||
.sb-field:focus {
|
||||
border-color: var(--nak-raspberry);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 3px rgba(215, 50, 130, 0.15);
|
||||
}
|
||||
|
||||
.sb-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-height: 40px;
|
||||
padding: 10px 20px;
|
||||
border: 1px solid var(--gray-300);
|
||||
background: #ffffff;
|
||||
color: var(--gray-700);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition: background 150ms ease, transform 150ms ease, box-shadow 150ms ease;
|
||||
}
|
||||
|
||||
.sb-btn:hover {
|
||||
background: var(--gray-100);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.sb-btn-primary {
|
||||
border-color: var(--nak-raspberry);
|
||||
background: linear-gradient(135deg, var(--nak-raspberry), #b8296e);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.sb-btn-primary:hover { border-color: #b8296e; background: linear-gradient(135deg, #b8296e, #9c2260); }
|
||||
|
||||
.sb-btn-danger {
|
||||
min-height: 30px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--gray-300);
|
||||
background: none;
|
||||
color: var(--nak-red);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.sb-btn-danger:hover { border-color: var(--nak-red); background: hsl(0, 78%, 96%); }
|
||||
|
||||
.sb-alert {
|
||||
margin: var(--space-md) 0;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid hsl(145, 50%, 85%);
|
||||
background: hsl(145, 50%, 93%);
|
||||
color: hsl(145, 50%, 28%);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sb-alert-error {
|
||||
border-color: hsl(0, 70%, 86%);
|
||||
background: hsl(0, 78%, 96%);
|
||||
color: hsl(0, 65%, 38%);
|
||||
}
|
||||
|
||||
.sb-aushang-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: var(--space-md);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.sb-aushang-card {
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--gray-200);
|
||||
background: var(--gray-50);
|
||||
transition: box-shadow 200ms ease, transform 200ms ease;
|
||||
}
|
||||
|
||||
.sb-aushang-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }
|
||||
|
||||
.sb-aushang-card img {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
object-fit: contain;
|
||||
background: white;
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.sb-aushang-delete-form { width: 100%; }
|
||||
|
||||
.sb-spinner {
|
||||
display: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0 auto;
|
||||
border: 3px solid var(--gray-200);
|
||||
border-top-color: var(--nak-raspberry);
|
||||
border-radius: 50%;
|
||||
animation: sbSpin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.sb-uploading .sb-upload-button { display: none; }
|
||||
|
||||
.sb-uploading .sb-spinner { display: block; }
|
||||
|
||||
|
||||
@keyframes sbSpin { to { transform: rotate(360deg); } }
|
||||
|
||||
.sb-format-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sb-format-option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--gray-200);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sb-format-option:has(input:checked) { border-color: var(--nak-raspberry); box-shadow: inset 0 0 0 1px var(--nak-raspberry); }
|
||||
|
||||
.sb-format-option input { margin-top: 3px; }
|
||||
|
||||
.sb-format-option span { display: grid; gap: 3px; }
|
||||
|
||||
.sb-format-option small { color: var(--gray-500); line-height: 1.35; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sb-page { padding: var(--space-md) var(--space-sm); }
|
||||
.sb-page > .content-tab-bar {
|
||||
margin: calc(-1 * var(--space-md)) calc(-1 * var(--space-sm)) 0;
|
||||
width: calc(100% + 2 * var(--space-sm));
|
||||
}
|
||||
.sb-card { padding: 16px; }
|
||||
.sb-form-grid, .sb-format-options { grid-template-columns: 1fr; }
|
||||
.sb-aushang-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
|
||||
}
|
||||
|
||||
@@ -1,47 +1,23 @@
|
||||
(function () {
|
||||
function switchTab(tabName, updateHash) {
|
||||
const tab = document.querySelector('.content-tab-bar__item[data-tab="' + tabName + '"]');
|
||||
const content = document.getElementById('tab-' + tabName);
|
||||
if (!tab || !content) return;
|
||||
|
||||
document.querySelectorAll('.content-tab-bar__item').forEach(function (item) {
|
||||
item.classList.toggle('active', item === tab);
|
||||
});
|
||||
document.querySelectorAll('.sb-tab-content').forEach(function (item) {
|
||||
item.classList.toggle('active', item === content);
|
||||
});
|
||||
|
||||
if (updateHash) {
|
||||
history.replaceState(null, '', '#' + tabName);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('.content-tab-bar__item[data-tab]').forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
switchTab(tab.dataset.tab, true);
|
||||
});
|
||||
});
|
||||
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (hash) {
|
||||
switchTab(hash, false);
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-confirm]').forEach(function (button) {
|
||||
button.addEventListener('click', function (event) {
|
||||
if (!window.confirm(button.dataset.confirm || 'Wirklich fortfahren?')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-upload-form]').forEach(function (form) {
|
||||
form.addEventListener('submit', function () {
|
||||
form.classList.add('sb-uploading');
|
||||
const button = form.querySelector('.sb-upload-button');
|
||||
if (button) button.disabled = true;
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const tabs = [...document.querySelectorAll('.content-tab-bar__item[data-tab]')];
|
||||
const panels = [...document.querySelectorAll('.sb-tab-content')];
|
||||
function showTab(name) {
|
||||
if (!tabs.some(tab => tab.dataset.tab === name)) return;
|
||||
tabs.forEach(tab => tab.classList.toggle('active', tab.dataset.tab === name));
|
||||
panels.forEach(panel => panel.classList.toggle('active', panel.id === 'tab-' + name));
|
||||
history.replaceState(null, '', '#' + name);
|
||||
}
|
||||
tabs.forEach(tab => tab.addEventListener('click', () => showTab(tab.dataset.tab)));
|
||||
showTab(location.hash.slice(1));
|
||||
document.querySelectorAll('[data-confirm]').forEach(button => {
|
||||
button.addEventListener('click', event => {
|
||||
if (!confirm(button.dataset.confirm)) event.preventDefault();
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('[data-upload-form]').forEach(form => {
|
||||
form.addEventListener('submit', () => {
|
||||
form.classList.add('sb-uploading');
|
||||
form.querySelector('[type="submit"]').disabled = true;
|
||||
});
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
||||
@@ -1,327 +1,228 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/initialise.php';
|
||||
require_once __DIR__ . '/util/contentpanel_helpers.php';
|
||||
|
||||
$generalPermission = $groups['Allgemein'] ?? null;
|
||||
if (!in_array($generalPermission, [1, '1', true, 't', 'true'], true)) {
|
||||
header('Location: /intranet_repo/projects/intranet/index.php', true, 303);
|
||||
exit();
|
||||
}
|
||||
|
||||
contentpanel_cleanup_expired_news();
|
||||
$csrfToken = contentpanel_csrf_token();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = trim((string) ($_POST['action'] ?? ''));
|
||||
$tab = contentpanel_action_tab($action);
|
||||
$flashType = 'success';
|
||||
|
||||
try {
|
||||
contentpanel_verify_csrf($_POST['csrf_token'] ?? null);
|
||||
|
||||
$message = match ($action) {
|
||||
'import_leitgedanken' => contentpanel_import_leitgedanken($pdo, $_FILES['leitgedanken_pdf'] ?? []),
|
||||
'create_service' => contentpanel_create_service($pdo, $_POST),
|
||||
'delete_service' => contentpanel_delete_service($pdo, $_POST['id'] ?? null),
|
||||
'create_announcement' => contentpanel_create_announcement($pdo, $_POST),
|
||||
'delete_announcement' => contentpanel_delete_announcement($pdo, $_POST['id'] ?? null),
|
||||
'upload_news' => contentpanel_upload_news($_POST, $_FILES['fileToUpload'] ?? []),
|
||||
'delete_news' => contentpanel_delete_news_files((string) ($_POST['file_base'] ?? ''))
|
||||
? 'Aushang wurde vollständig gelöscht.'
|
||||
: throw new InvalidArgumentException('Ungültiger Dateiname oder Aushang konnte nicht vollständig gelöscht werden.'),
|
||||
default => throw new InvalidArgumentException('Unbekannte Aktion.'),
|
||||
};
|
||||
} catch (InvalidArgumentException $exception) {
|
||||
$message = $exception->getMessage();
|
||||
$flashType = 'error';
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Contentpanel action failed (' . $action . '): ' . $exception->getMessage());
|
||||
$message = 'Die Aktion konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut.';
|
||||
$flashType = 'error';
|
||||
}
|
||||
|
||||
$_SESSION['contentpanel_flash'] = [
|
||||
'tab' => $tab,
|
||||
'message' => $message,
|
||||
'type' => $flashType,
|
||||
];
|
||||
|
||||
header('Location: insert.php#' . rawurlencode($tab), true, 303);
|
||||
exit();
|
||||
}
|
||||
|
||||
$flash = $_SESSION['contentpanel_flash'] ?? null;
|
||||
unset($_SESSION['contentpanel_flash']);
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$services = contentpanel_fetch_services($pdo, $today);
|
||||
$announcements = contentpanel_fetch_announcements($pdo, $today);
|
||||
$newsItems = contentpanel_list_news();
|
||||
$pdo = null;
|
||||
|
||||
$flashTab = is_array($flash) ? (string) ($flash['tab'] ?? '') : '';
|
||||
$flashMessage = is_array($flash) ? (string) ($flash['message'] ?? '') : '';
|
||||
$flashType = is_array($flash) ? (string) ($flash['type'] ?? '') : '';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" dir="ltr">
|
||||
<head>
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/projects/_central/php/html_head.php'; ?>
|
||||
<link rel="stylesheet" href="/intranet_repo/projects/_central/layouts/contentpanel.css">
|
||||
<script src="/intranet_repo/projects/_central/scripts/contentpanel.js" defer></script>
|
||||
</head>
|
||||
<body class="theme-raspberry">
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/projects/_central/php/header.php'; ?>
|
||||
|
||||
<div class="header_title ht_raspberry text_white" id="header_title">
|
||||
<span class="header_title_text">Schwarzes Brett</span>
|
||||
</div>
|
||||
|
||||
<div class="sb-page">
|
||||
<div class="content-tab-bar" role="tablist" aria-label="Schwarzes Brett">
|
||||
<button type="button" class="content-tab-bar__item active" data-tab="gottesdienst">Gottesdienst</button>
|
||||
<button type="button" class="content-tab-bar__item" data-tab="ansagen">Ansagen</button>
|
||||
<button type="button" class="content-tab-bar__item" data-tab="dateien">Dateiverwaltung</button>
|
||||
</div>
|
||||
|
||||
<section class="sb-tab-content active" id="tab-gottesdienst">
|
||||
<?php if ($flashTab === 'gottesdienst' && $flashMessage !== ''): ?>
|
||||
<div class="sb-alert<?= $flashType === 'error' ? ' sb-alert-error' : '' ?>" role="status">
|
||||
<?= contentpanel_html($flashMessage) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="sb-card">
|
||||
<div class="sb-card-title">
|
||||
<svg viewBox="0 0 24 24"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11z"/></svg>
|
||||
Geplante Gottesdienste
|
||||
</div>
|
||||
<div class="sb-entry-list">
|
||||
<?php if ($services === []): ?>
|
||||
<div class="sb-empty">Keine Einträge vorhanden.</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($services as $row): ?>
|
||||
<div class="sb-entry">
|
||||
<div class="sb-entry-info">
|
||||
<div class="sb-entry-date"><?= contentpanel_html($row['datum']) ?></div>
|
||||
<div class="sb-entry-title"><?= contentpanel_html($row['bibelwort']) ?></div>
|
||||
<div class="sb-entry-subtitle"><?= contentpanel_html($row['bibelvers']) ?></div>
|
||||
</div>
|
||||
<form method="post" action="insert.php">
|
||||
<input type="hidden" name="csrf_token" value="<?= contentpanel_html($csrfToken) ?>">
|
||||
<input type="hidden" name="action" value="delete_service">
|
||||
<input type="hidden" name="id" value="<?= (int) $row['id'] ?>">
|
||||
<button type="submit" class="sb-btn sb-btn-danger" data-confirm="Eintrag wirklich löschen?">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sb-card">
|
||||
<div class="sb-card-title">
|
||||
<svg viewBox="0 0 24 24"><path d="M9 16h6v-6h4l-7-7-7 7h4v6zm-4 2h14v2H5v-2z"/></svg>
|
||||
Leitgedanken importieren
|
||||
</div>
|
||||
<form action="insert.php" method="post" enctype="multipart/form-data" data-upload-form>
|
||||
<input type="hidden" name="csrf_token" value="<?= contentpanel_html($csrfToken) ?>">
|
||||
<input type="hidden" name="action" value="import_leitgedanken">
|
||||
<div class="sb-form-grid sb-form-grid-single">
|
||||
<div class="sb-field-group">
|
||||
<label class="sb-label" for="leitgedanken_pdf">PDF hochladen (LG_*.pdf)</label>
|
||||
<input class="sb-field" id="leitgedanken_pdf" type="file" name="leitgedanken_pdf" accept=".pdf,application/pdf" required>
|
||||
</div>
|
||||
<div class="sb-field-group">
|
||||
<button type="submit" class="sb-btn sb-btn-primary sb-upload-button">Import starten</button>
|
||||
<span class="sb-spinner" aria-label="Import läuft"></span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="sb-card">
|
||||
<div class="sb-card-title">
|
||||
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
||||
Neuen Gottesdienst planen
|
||||
</div>
|
||||
<form method="post" action="insert.php">
|
||||
<input type="hidden" name="csrf_token" value="<?= contentpanel_html($csrfToken) ?>">
|
||||
<input type="hidden" name="action" value="create_service">
|
||||
<div class="sb-form-grid">
|
||||
<div class="sb-field-group sb-form-full">
|
||||
<label class="sb-label" for="bibeldatum">Datum des Gottesdienstes <span class="sb-required">*</span></label>
|
||||
<input class="sb-field" id="bibeldatum" type="date" name="bibeldatum" required>
|
||||
</div>
|
||||
<div class="sb-field-group">
|
||||
<label class="sb-label" for="bibelwort">Bibelstelle <span class="sb-required">*</span></label>
|
||||
<input class="sb-field" id="bibelwort" type="text" name="bibelwort" required placeholder="z.B. Joh 3,16">
|
||||
</div>
|
||||
<div class="sb-field-group">
|
||||
<label class="sb-label" for="bibelvers">Bibelvers <span class="sb-required">*</span></label>
|
||||
<input class="sb-field" id="bibelvers" type="text" name="bibelvers" required placeholder="Bibelvers eingeben">
|
||||
</div>
|
||||
<div class="sb-field-group sb-form-full sb-field-group-action">
|
||||
<button type="submit" class="sb-btn sb-btn-primary">Eintragen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sb-tab-content" id="tab-ansagen">
|
||||
<?php if ($flashTab === 'ansagen' && $flashMessage !== ''): ?>
|
||||
<div class="sb-alert<?= $flashType === 'error' ? ' sb-alert-error' : '' ?>" role="status">
|
||||
<?= contentpanel_html($flashMessage) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="sb-card">
|
||||
<div class="sb-card-title">
|
||||
<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>
|
||||
Geplante Ansagen
|
||||
</div>
|
||||
<div class="sb-entry-list">
|
||||
<?php if ($announcements === []): ?>
|
||||
<div class="sb-empty">Keine Einträge vorhanden.</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($announcements as $row): ?>
|
||||
<div class="sb-entry">
|
||||
<div class="sb-entry-info">
|
||||
<div class="sb-entry-date">
|
||||
<?= contentpanel_html($row['datum_von']) ?><?php if ($row['datum_bis'] !== ''): ?> – <?= contentpanel_html($row['datum_bis']) ?><?php endif; ?>
|
||||
</div>
|
||||
<div class="sb-entry-title"><?= contentpanel_html($row['ansage_titel']) ?></div>
|
||||
<?php if (!empty($row['ansage_untertitel'])): ?>
|
||||
<div class="sb-entry-subtitle"><?= contentpanel_html($row['ansage_untertitel']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($row['uhrzeit'] !== '' || !empty($row['ansage_ort'])): ?>
|
||||
<div class="sb-entry-meta">
|
||||
<?php if ($row['uhrzeit'] !== ''): ?><?= contentpanel_html($row['uhrzeit']) ?> Uhr<?php endif; ?>
|
||||
<?php if ($row['uhrzeit'] !== '' && !empty($row['ansage_ort'])): ?> · <?php endif; ?>
|
||||
<?php if (!empty($row['ansage_ort'])): ?><?= contentpanel_html($row['ansage_ort']) ?><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<form method="post" action="insert.php">
|
||||
<input type="hidden" name="csrf_token" value="<?= contentpanel_html($csrfToken) ?>">
|
||||
<input type="hidden" name="action" value="delete_announcement">
|
||||
<input type="hidden" name="id" value="<?= (int) $row['id'] ?>">
|
||||
<button type="submit" class="sb-btn sb-btn-danger" data-confirm="Ansage wirklich löschen?">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sb-card">
|
||||
<div class="sb-card-title">
|
||||
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
||||
Neue Ansage planen
|
||||
</div>
|
||||
<form method="post" action="insert.php">
|
||||
<input type="hidden" name="csrf_token" value="<?= contentpanel_html($csrfToken) ?>">
|
||||
<input type="hidden" name="action" value="create_announcement">
|
||||
<div class="sb-form-grid">
|
||||
<div class="sb-field-group sb-form-full">
|
||||
<label class="sb-label" for="ansage_titel">Titel <span class="sb-required">*</span></label>
|
||||
<input class="sb-field" id="ansage_titel" type="text" name="ansage_titel" required placeholder="Titel der Ansage">
|
||||
</div>
|
||||
<div class="sb-field-group">
|
||||
<label class="sb-label" for="ansage_datum_von">Datum <span class="sb-required">*</span></label>
|
||||
<input class="sb-field" id="ansage_datum_von" type="date" name="ansage_datum_von" required>
|
||||
</div>
|
||||
<div class="sb-field-group">
|
||||
<label class="sb-label" for="ansage_datum_bis">Datum bis <span class="sb-optional">(optional)</span></label>
|
||||
<input class="sb-field" id="ansage_datum_bis" type="date" name="ansage_datum_bis">
|
||||
</div>
|
||||
<div class="sb-field-group">
|
||||
<label class="sb-label" for="ansage_uhrzeit">Uhrzeit <span class="sb-optional">(optional)</span></label>
|
||||
<input class="sb-field" id="ansage_uhrzeit" type="time" name="ansage_uhrzeit">
|
||||
</div>
|
||||
<div class="sb-field-group">
|
||||
<label class="sb-label" for="ansage_ort">Ort <span class="sb-optional">(optional)</span></label>
|
||||
<input class="sb-field" id="ansage_ort" type="text" name="ansage_ort" placeholder="z.B. Gemeindesaal">
|
||||
</div>
|
||||
<div class="sb-field-group sb-form-full">
|
||||
<label class="sb-label" for="ansage_untertitel">Weitere Informationen <span class="sb-optional">(optional)</span></label>
|
||||
<input class="sb-field" id="ansage_untertitel" type="text" name="ansage_untertitel" placeholder="Zusätzliche Hinweise">
|
||||
</div>
|
||||
<div class="sb-field-group sb-form-full sb-field-group-action">
|
||||
<button type="submit" class="sb-btn sb-btn-primary">Eintragen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sb-tab-content" id="tab-dateien">
|
||||
<?php if ($flashTab === 'dateien' && $flashMessage !== ''): ?>
|
||||
<div class="sb-alert<?= $flashType === 'error' ? ' sb-alert-error' : '' ?>" role="status">
|
||||
<?= contentpanel_html($flashMessage) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="sb-card">
|
||||
<div class="sb-card-title">
|
||||
<svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
|
||||
Aktuelle Aushänge
|
||||
</div>
|
||||
<div class="sb-aushang-grid">
|
||||
<?php if ($newsItems === []): ?>
|
||||
<div class="sb-empty">Keine Aushänge vorhanden.</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($newsItems as $item): ?>
|
||||
<div class="sb-aushang-card">
|
||||
<img src="<?= contentpanel_html($item['image_url']) ?>" alt="Aushang">
|
||||
<div class="sb-aushang-title"><?= contentpanel_html($item['title']) ?></div>
|
||||
<div class="sb-aushang-meta">
|
||||
<?= (int) $item['pages'] ?> <?= (int) $item['pages'] === 1 ? 'Seite' : 'Seiten' ?> · Verfällt <?= contentpanel_html($item['expiry']) ?>
|
||||
</div>
|
||||
<form method="post" action="insert.php" class="sb-aushang-delete-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= contentpanel_html($csrfToken) ?>">
|
||||
<input type="hidden" name="action" value="delete_news">
|
||||
<input type="hidden" name="file_base" value="<?= contentpanel_html($item['base']) ?>">
|
||||
<button type="submit" class="sb-btn sb-btn-danger" data-confirm="Aushang wirklich löschen?">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sb-card">
|
||||
<div class="sb-card-title">
|
||||
<svg viewBox="0 0 24 24"><path d="M9 16h6v-6h4l-7-7-7 7h4v6zm-4 2h14v2H5v-2z"/></svg>
|
||||
Neuen Aushang hochladen
|
||||
</div>
|
||||
<form action="insert.php" method="post" enctype="multipart/form-data" data-upload-form>
|
||||
<input type="hidden" name="csrf_token" value="<?= contentpanel_html($csrfToken) ?>">
|
||||
<input type="hidden" name="action" value="upload_news">
|
||||
<div class="sb-form-grid">
|
||||
<div class="sb-field-group">
|
||||
<label class="sb-label" for="news_date">Verfallsdatum <span class="sb-required">*</span></label>
|
||||
<input class="sb-field" id="news_date" type="date" name="date" required>
|
||||
</div>
|
||||
<div class="sb-field-group">
|
||||
<label class="sb-label" for="news_name">Titel des Aushangs <span class="sb-required">*</span></label>
|
||||
<input class="sb-field" id="news_name" type="text" name="name" required placeholder="Name des Dokuments">
|
||||
</div>
|
||||
<div class="sb-field-group sb-form-full">
|
||||
<label class="sb-label" for="fileToUpload">PDF-Datei auswählen <span class="sb-required">*</span></label>
|
||||
<input class="sb-field" id="fileToUpload" type="file" name="fileToUpload" accept=".pdf,application/pdf" required>
|
||||
</div>
|
||||
<div class="sb-field-group sb-form-full sb-field-group-action">
|
||||
<button type="submit" class="sb-btn sb-btn-primary sb-upload-button">Datei hochladen</button>
|
||||
<span class="sb-spinner" aria-label="Upload läuft"></span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/projects/_central/php/footer.php'; ?>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/initialise.php';
|
||||
require_once __DIR__ . '/util/contentpanel_helpers.php';
|
||||
require_once __DIR__ . '/util/view.php';
|
||||
|
||||
$generalPermission = $groups['Allgemein'] ?? null;
|
||||
if (!in_array($generalPermission, [1, '1', true, 't', 'true'], true)) {
|
||||
header('Location: /intranet_repo/projects/intranet/index.php', true, 303);
|
||||
exit();
|
||||
}
|
||||
|
||||
contentpanel_cleanup_expired_news();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = trim((string) ($_POST['action'] ?? ''));
|
||||
$tab = contentpanel_action_tab($action);
|
||||
$flashType = 'success';
|
||||
|
||||
try {
|
||||
contentpanel_verify_csrf($_POST['csrf_token'] ?? null);
|
||||
|
||||
$message = match ($action) {
|
||||
'import_leitgedanken' => contentpanel_import_leitgedanken($pdo, $_FILES['leitgedanken_pdf'] ?? []),
|
||||
'create_service' => contentpanel_create_service($pdo, $_POST),
|
||||
'delete_service', 'delete_announcement' => contentpanel_delete_entry($pdo, $action, $_POST['id'] ?? null),
|
||||
'create_announcement' => contentpanel_create_announcement($pdo, $_POST),
|
||||
'upload_news' => contentpanel_upload_news($_POST, $_FILES['fileToUpload'] ?? [], $pdo, $user),
|
||||
'delete_news' => contentpanel_delete_news_files((string) ($_POST['file_base'] ?? ''))
|
||||
? 'Aushang wurde gelöscht.'
|
||||
: throw new InvalidArgumentException('Ungültiger Dateiname oder Aushang konnte nicht vollständig gelöscht werden.'),
|
||||
default => throw new InvalidArgumentException('Unbekannte Aktion.'),
|
||||
};
|
||||
} catch (InvalidArgumentException $exception) {
|
||||
$message = $exception->getMessage();
|
||||
$flashType = 'error';
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Contentpanel action failed (' . $action . '): ' . $exception->getMessage());
|
||||
$message = 'Die Aktion konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut.';
|
||||
$flashType = 'error';
|
||||
}
|
||||
|
||||
$_SESSION['contentpanel_flash'] = [
|
||||
'tab' => $tab,
|
||||
'message' => $message,
|
||||
'type' => $flashType,
|
||||
];
|
||||
|
||||
header('Location: insert.php#' . rawurlencode($tab), true, 303);
|
||||
exit();
|
||||
}
|
||||
|
||||
$flash = $_SESSION['contentpanel_flash'] ?? null;
|
||||
unset($_SESSION['contentpanel_flash']);
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$services = contentpanel_fetch_services($pdo, $today);
|
||||
$announcements = contentpanel_fetch_announcements($pdo, $today);
|
||||
$newsItems = contentpanel_list_news();
|
||||
$pdo = null;
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" dir="ltr">
|
||||
<head>
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/projects/_central/php/html_head.php'; ?>
|
||||
<link rel="stylesheet" href="/intranet_repo/projects/_central/layouts/contentpanel.css?v=<?= (int) filemtime(__DIR__ . '/../../_central/layouts/contentpanel.css') ?>">
|
||||
<script src="/intranet_repo/projects/_central/scripts/contentpanel.js?v=<?= (int) filemtime(__DIR__ . '/../../_central/scripts/contentpanel.js') ?>" defer></script>
|
||||
</head>
|
||||
<body class="theme-raspberry">
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/projects/_central/php/header.php'; ?>
|
||||
|
||||
<div class="header_title ht_raspberry text_white" id="header_title">
|
||||
<span class="header_title_text">Schwarzes Brett</span>
|
||||
</div>
|
||||
|
||||
<div class="sb-page">
|
||||
<div class="content-tab-bar" role="tablist" aria-label="Schwarzes Brett">
|
||||
<button type="button" class="content-tab-bar__item active" data-tab="gottesdienst">Gottesdienst</button>
|
||||
<button type="button" class="content-tab-bar__item" data-tab="ansagen">Ansagen</button>
|
||||
<button type="button" class="content-tab-bar__item" data-tab="dateien">Dateiverwaltung</button>
|
||||
</div>
|
||||
|
||||
<section class="sb-tab-content active" id="tab-gottesdienst">
|
||||
<?php contentpanel_flash($flash, 'gottesdienst'); ?>
|
||||
|
||||
<div class="sb-card">
|
||||
<div class="sb-card-title">
|
||||
<svg viewBox="0 0 24 24"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11z"/></svg>
|
||||
Geplante Gottesdienste
|
||||
</div>
|
||||
<?php contentpanel_entries($services, 'delete_service'); ?>
|
||||
</div>
|
||||
|
||||
<div class="sb-card">
|
||||
<div class="sb-card-title">
|
||||
<svg viewBox="0 0 24 24"><path d="M9 16h6v-6h4l-7-7-7 7h4v6zm-4 2h14v2H5v-2z"/></svg>
|
||||
Leitgedanken importieren
|
||||
</div>
|
||||
<form action="insert.php" method="post" enctype="multipart/form-data" data-upload-form>
|
||||
<?php contentpanel_hidden('import_leitgedanken'); ?>
|
||||
<div class="sb-form-grid sb-form-grid-single">
|
||||
<?php contentpanel_field('leitgedanken_pdf', 'PDF hochladen (LG_*.pdf)', ['type' => 'file', 'required' => true, 'accept' => '.pdf,application/pdf']); ?>
|
||||
<div class="sb-field-group">
|
||||
<button type="submit" class="sb-btn sb-btn-primary sb-upload-button">Import starten</button>
|
||||
<span class="sb-spinner" aria-label="Import läuft"></span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="sb-card">
|
||||
<div class="sb-card-title">
|
||||
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
||||
Neuen Gottesdienst planen
|
||||
</div>
|
||||
<form method="post" action="insert.php">
|
||||
<?php contentpanel_hidden('create_service'); ?>
|
||||
<div class="sb-form-grid">
|
||||
<?php contentpanel_field('bibeldatum', 'Datum des Gottesdienstes', ['type' => 'date', 'required' => true], true); ?>
|
||||
<?php contentpanel_field('bibelwort', 'Bibelstelle', ['required' => true, 'placeholder' => 'z.B. Joh 3,16']); ?>
|
||||
<?php contentpanel_field('bibelvers', 'Bibelvers', ['required' => true, 'placeholder' => 'Bibelvers eingeben']); ?>
|
||||
<div class="sb-field-group sb-form-full sb-field-group-action">
|
||||
<button type="submit" class="sb-btn sb-btn-primary">Eintragen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sb-tab-content" id="tab-ansagen">
|
||||
<?php contentpanel_flash($flash, 'ansagen'); ?>
|
||||
|
||||
<div class="sb-card">
|
||||
<div class="sb-card-title">
|
||||
<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>
|
||||
Geplante Ansagen
|
||||
</div>
|
||||
<?php contentpanel_entries($announcements, 'delete_announcement'); ?>
|
||||
</div>
|
||||
|
||||
<div class="sb-card">
|
||||
<div class="sb-card-title">
|
||||
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
||||
Neue Ansage planen
|
||||
</div>
|
||||
<form method="post" action="insert.php">
|
||||
<?php contentpanel_hidden('create_announcement'); ?>
|
||||
<div class="sb-form-grid">
|
||||
<?php contentpanel_field('ansage_titel', 'Titel', ['required' => true, 'placeholder' => 'Titel der Ansage'], true); ?>
|
||||
<?php contentpanel_field('ansage_datum_von', 'Datum', ['type' => 'date', 'required' => true]); ?>
|
||||
<?php contentpanel_field('ansage_datum_bis', 'Datum bis', ['type' => 'date']); ?>
|
||||
<?php contentpanel_field('ansage_uhrzeit', 'Uhrzeit', ['type' => 'time']); ?>
|
||||
<?php contentpanel_field('ansage_ort', 'Ort', ['placeholder' => 'z.B. Gemeindesaal']); ?>
|
||||
<?php contentpanel_field('ansage_untertitel', 'Weitere Informationen', ['placeholder' => 'Zusätzliche Hinweise'], true); ?>
|
||||
<div class="sb-field-group sb-form-full sb-field-group-action">
|
||||
<button type="submit" class="sb-btn sb-btn-primary">Eintragen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sb-tab-content" id="tab-dateien">
|
||||
<?php contentpanel_flash($flash, 'dateien'); ?>
|
||||
|
||||
<div class="sb-card">
|
||||
<div class="sb-card-title">
|
||||
<svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
|
||||
Aktuelle Aushänge
|
||||
</div>
|
||||
<div class="sb-aushang-grid">
|
||||
<?php if ($newsItems === []): ?>
|
||||
<div class="sb-empty">Keine Aushänge vorhanden.</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($newsItems as $item): ?>
|
||||
<div class="sb-aushang-card">
|
||||
<img src="<?= contentpanel_html($item['image_url']) ?>" alt="<?= contentpanel_html($item['title']) ?>" loading="lazy">
|
||||
<div class="sb-aushang-title"><?= contentpanel_html($item['title']) ?></div>
|
||||
<div class="sb-aushang-meta">
|
||||
<?= $item['pages'] ?> <?= $item['pages'] === 1 ? 'Seite' : 'Seiten' ?> · Verfällt <?= contentpanel_html($item['expiry']) ?>
|
||||
</div>
|
||||
<form method="post" action="insert.php" class="sb-aushang-delete-form">
|
||||
<?php contentpanel_hidden('delete_news', ['file_base' => $item['base']]); ?>
|
||||
<button type="submit" class="sb-btn sb-btn-danger" data-confirm="Aushang wirklich löschen?">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sb-card">
|
||||
<div class="sb-card-title">
|
||||
<svg viewBox="0 0 24 24"><path d="M9 16h6v-6h4l-7-7-7 7h4v6zm-4 2h14v2H5v-2z"/></svg>
|
||||
Neuen Aushang hochladen
|
||||
</div>
|
||||
<form action="insert.php" method="post" enctype="multipart/form-data" data-upload-form>
|
||||
<?php contentpanel_hidden('upload_news'); ?>
|
||||
<div class="sb-form-grid">
|
||||
<?php contentpanel_field('date', 'Verfallsdatum', ['type' => 'date', 'id' => 'news_date', 'required' => true]); ?>
|
||||
<?php contentpanel_field('name', 'Titel des Aushangs', ['id' => 'news_name', 'required' => true, 'placeholder' => 'Name des Dokuments']); ?>
|
||||
<div class="sb-field-group sb-form-full">
|
||||
<span class="sb-label">Darstellung</span>
|
||||
<div class="sb-format-options" role="radiogroup" aria-label="Darstellung des Aushangs">
|
||||
<label class="sb-format-option">
|
||||
<input type="radio" name="format_mode" value="template" checked>
|
||||
<span><strong>In NAK-Formatvorlage überführen</strong><small>Dokument wird automatisch in unser einheitliches Aushang-Layout gesetzt.</small></span>
|
||||
</label>
|
||||
<label class="sb-format-option">
|
||||
<input type="radio" name="format_mode" value="original">
|
||||
<span><strong>Original-PDF verwenden</strong><small>Das bestehende Layout bleibt unverändert.</small></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php contentpanel_field('fileToUpload', 'PDF-Datei auswählen', ['type' => 'file', 'required' => true, 'accept' => '.pdf,application/pdf'], true); ?>
|
||||
<div class="sb-field-group sb-form-full sb-field-group-action">
|
||||
<button type="submit" class="sb-btn sb-btn-primary sb-upload-button">Datei hochladen</button>
|
||||
<span class="sb-spinner" aria-label="Upload läuft"></span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/projects/_central/php/footer.php'; ?>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
@@ -1,574 +1,299 @@
|
||||
<?php
|
||||
|
||||
const CONTENTPANEL_NEWS_PNG_DIRECTORY = __DIR__ . '/../news/png';
|
||||
|
||||
function contentpanel_html($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function contentpanel_csrf_token(): string
|
||||
{
|
||||
if (empty($_SESSION['contentpanel_csrf_token'])) {
|
||||
$_SESSION['contentpanel_csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
return (string) $_SESSION['contentpanel_csrf_token'];
|
||||
}
|
||||
|
||||
function contentpanel_verify_csrf($postedToken): void
|
||||
{
|
||||
$sessionToken = $_SESSION['contentpanel_csrf_token'] ?? '';
|
||||
|
||||
if (
|
||||
!is_string($postedToken)
|
||||
|| !is_string($sessionToken)
|
||||
|| $sessionToken === ''
|
||||
|| !hash_equals($sessionToken, $postedToken)
|
||||
) {
|
||||
throw new InvalidArgumentException('Ungültige Anfrage. Bitte laden Sie die Seite neu.');
|
||||
}
|
||||
}
|
||||
|
||||
function contentpanel_action_tab(string $action): string
|
||||
{
|
||||
return match ($action) {
|
||||
'create_announcement', 'delete_announcement' => 'ansagen',
|
||||
'upload_news', 'delete_news' => 'dateien',
|
||||
default => 'gottesdienst',
|
||||
};
|
||||
}
|
||||
|
||||
function contentpanel_required_text(array $input, string $key, string $message): string
|
||||
{
|
||||
$value = trim((string) ($input[$key] ?? ''));
|
||||
if ($value === '') {
|
||||
throw new InvalidArgumentException($message);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
function contentpanel_date($value, string $message): DateTimeImmutable
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $raw);
|
||||
$errors = DateTimeImmutable::getLastErrors();
|
||||
|
||||
if (
|
||||
$date === false
|
||||
|| ($errors !== false && ($errors['warning_count'] > 0 || $errors['error_count'] > 0))
|
||||
|| $date->format('Y-m-d') !== $raw
|
||||
) {
|
||||
throw new InvalidArgumentException($message);
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
function contentpanel_optional_date($value, string $message): ?DateTimeImmutable
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
return $raw === '' ? null : contentpanel_date($raw, $message);
|
||||
}
|
||||
|
||||
function contentpanel_optional_time($value, string $message): ?string
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/D', $raw) !== 1) {
|
||||
throw new InvalidArgumentException($message);
|
||||
}
|
||||
|
||||
return $raw;
|
||||
}
|
||||
|
||||
function contentpanel_positive_id($value): int
|
||||
{
|
||||
$id = filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
|
||||
if ($id === false) {
|
||||
throw new InvalidArgumentException('Ungültiger Eintrag.');
|
||||
}
|
||||
|
||||
return (int) $id;
|
||||
}
|
||||
|
||||
function contentpanel_format_message_date(DateTimeInterface $date): string
|
||||
{
|
||||
static $months = [
|
||||
1 => 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
|
||||
];
|
||||
|
||||
return sprintf('%02d. %s', (int) $date->format('d'), $months[(int) $date->format('n')]);
|
||||
}
|
||||
|
||||
function contentpanel_format_display_date($value): string
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
if ($raw === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$date = DateTimeImmutable::createFromFormat('!Y-m-d', substr($raw, 0, 10));
|
||||
return $date ? $date->format('d.m.Y') : $raw;
|
||||
}
|
||||
|
||||
function contentpanel_find_binary(string $name): ?string
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/news.php';
|
||||
require_once __DIR__ . '/news_queue.php';
|
||||
|
||||
function contentpanel_html($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function contentpanel_csrf_token(): string
|
||||
{
|
||||
return $_SESSION['contentpanel_csrf_token'] ??= bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
function contentpanel_verify_csrf($postedToken): void
|
||||
{
|
||||
$sessionToken = $_SESSION['contentpanel_csrf_token'] ?? '';
|
||||
if (
|
||||
!is_string($postedToken)
|
||||
|| !is_string($sessionToken)
|
||||
|| $sessionToken === ''
|
||||
|| !hash_equals($sessionToken, $postedToken)
|
||||
) {
|
||||
throw new InvalidArgumentException('Ungültige Anfrage. Bitte laden Sie die Seite neu.');
|
||||
}
|
||||
}
|
||||
|
||||
function contentpanel_action_tab(string $action): string
|
||||
{
|
||||
return match ($action) {
|
||||
'create_announcement', 'delete_announcement' => 'ansagen',
|
||||
'upload_news', 'delete_news' => 'dateien',
|
||||
default => 'gottesdienst',
|
||||
};
|
||||
}
|
||||
|
||||
function contentpanel_required_text(array $input, string $key, string $message): string
|
||||
{
|
||||
$value = trim((string) ($input[$key] ?? ''));
|
||||
if ($value === '') {
|
||||
throw new InvalidArgumentException($message);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
function contentpanel_date($value, string $message): DateTimeImmutable
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $raw);
|
||||
if (!$date || $date->format('Y-m-d') !== $raw) {
|
||||
throw new InvalidArgumentException($message);
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
function contentpanel_optional_date($value, string $message): ?DateTimeImmutable
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
return $raw === '' ? null : contentpanel_date($raw, $message);
|
||||
}
|
||||
|
||||
function contentpanel_optional_time($value, string $message): ?string
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/D', $raw) !== 1) {
|
||||
throw new InvalidArgumentException($message);
|
||||
}
|
||||
|
||||
return $raw;
|
||||
}
|
||||
|
||||
function contentpanel_positive_id($value): int
|
||||
{
|
||||
$id = filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
|
||||
if ($id === false) {
|
||||
throw new InvalidArgumentException('Ungültiger Eintrag.');
|
||||
}
|
||||
|
||||
return (int) $id;
|
||||
}
|
||||
|
||||
function contentpanel_format_message_date(DateTimeInterface $date): string
|
||||
{
|
||||
static $months = [
|
||||
1 => 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
|
||||
];
|
||||
return sprintf('%02d. %s', (int) $date->format('d'), $months[(int) $date->format('n')]);
|
||||
}
|
||||
|
||||
function contentpanel_format_display_date($value): string
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
if ($raw === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$date = DateTimeImmutable::createFromFormat('!Y-m-d', substr($raw, 0, 10));
|
||||
return $date ? $date->format('d.m.Y') : $raw;
|
||||
}
|
||||
|
||||
function contentpanel_run(array $arguments, int $timeout = 90, array $userErrors = []): string
|
||||
{
|
||||
$command = implode(' ', array_map('escapeshellarg', ['timeout', (string) $timeout, ...$arguments]));
|
||||
$pipe = popen($command . ' 2>&1', 'r');
|
||||
if (!$pipe) throw new RuntimeException('Verarbeitung konnte nicht gestartet werden.');
|
||||
$output = [];
|
||||
$status = 0;
|
||||
exec('command -v ' . escapeshellarg($name) . ' 2>/dev/null', $output, $status);
|
||||
|
||||
if ($status !== 0 || empty($output[0])) {
|
||||
return null;
|
||||
while (($line = fgets($pipe)) !== false) {
|
||||
$output[] = rtrim($line, "\r\n");
|
||||
}
|
||||
|
||||
return trim((string) $output[0]);
|
||||
}
|
||||
|
||||
function contentpanel_validate_pdf_upload(array $file, string $uploadError, string $typeError): string
|
||||
{
|
||||
if ((int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
throw new InvalidArgumentException($uploadError);
|
||||
}
|
||||
|
||||
$name = (string) ($file['name'] ?? '');
|
||||
$tmpPath = (string) ($file['tmp_name'] ?? '');
|
||||
if (strtolower(pathinfo($name, PATHINFO_EXTENSION)) !== 'pdf' || !is_file($tmpPath)) {
|
||||
throw new InvalidArgumentException($typeError);
|
||||
}
|
||||
|
||||
$prefix = file_get_contents($tmpPath, false, null, 0, 1024);
|
||||
if ($prefix === false || strpos($prefix, '%PDF-') === false) {
|
||||
throw new InvalidArgumentException($typeError);
|
||||
}
|
||||
|
||||
return $tmpPath;
|
||||
}
|
||||
|
||||
function contentpanel_pdf_to_text(string $pdfPath): string
|
||||
{
|
||||
$binary = contentpanel_find_binary('pdftotext');
|
||||
if ($binary === null) {
|
||||
throw new RuntimeException("'pdftotext' wurde nicht gefunden.");
|
||||
}
|
||||
|
||||
$tempPath = tempnam(sys_get_temp_dir(), 'contentpanel_lg_');
|
||||
if ($tempPath === false) {
|
||||
throw new RuntimeException('Temporäre Datei konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
try {
|
||||
$command = escapeshellarg($binary)
|
||||
. ' -layout -enc UTF-8 '
|
||||
. escapeshellarg($pdfPath) . ' '
|
||||
. escapeshellarg($tempPath)
|
||||
. ' 2>&1';
|
||||
|
||||
$output = [];
|
||||
$status = 0;
|
||||
exec($command, $output, $status);
|
||||
if ($status !== 0) {
|
||||
throw new RuntimeException('pdftotext fehlgeschlagen: ' . implode("\n", $output));
|
||||
}
|
||||
|
||||
$text = file_get_contents($tempPath);
|
||||
if ($text === false) {
|
||||
throw new RuntimeException('Der extrahierte PDF-Text konnte nicht gelesen werden.');
|
||||
}
|
||||
|
||||
return $text;
|
||||
} finally {
|
||||
@unlink($tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
function contentpanel_parse_leitgedanken(string $rawText): array
|
||||
{
|
||||
$text = preg_replace("/\r\n?/", "\n", $rawText);
|
||||
$text = preg_replace("/(?<=\p{L})-\n(?=\p{L})/u", '', (string) $text);
|
||||
$lines = explode("\n", (string) $text);
|
||||
|
||||
$dateHeaderRegex = '/(?P<prefix>.*?)(?P<d1>\d{1,2})(?:\.\s*\/\s*(?P<d2>\d{1,2}))?\.(?P<m>\d{1,2})\.(?P<y>\d{4})\s*$/u';
|
||||
$books = '(?:Mo|Sam|Kön|Chr|Dan|Ps|Spr|Pred|Hld|Hi|Jes|Jer|Klgl|Hes|Ez|Hos|Joel|Am|Ob|Mi|Nah|Hab|Zef|Hag|Sach|Mal|Rut|Ri|Jos|Esra|Neh|Est|'
|
||||
. 'Mt|Mk|Lk|Joh|Apg|Röm|Kor|Gal|Eph|Phil|Kol|Thess|Tim|Tit|Phlm|Hebr|Jak|Petr|Joh|Jud|Offb)';
|
||||
$referenceRegex = '/(?P<ref>(?:[1-3]\.?\s*)?' . $books . '\s*\d{1,3}\s*,\s*\d{1,3}(?:[–\-]\d{1,3}(?:[a-z])?|(?:\.\d{1,3})+(?:[a-z])?|[a-z])?)\s*$/u';
|
||||
|
||||
$sections = [];
|
||||
foreach ($lines as $index => $line) {
|
||||
if (!preg_match($dateHeaderRegex, $line, $match)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$day = str_pad((string) (int) $match['d1'], 2, '0', STR_PAD_LEFT);
|
||||
$month = str_pad((string) (int) $match['m'], 2, '0', STR_PAD_LEFT);
|
||||
$sections[] = [
|
||||
'start' => $index,
|
||||
'date' => sprintf('%04d-%s-%s', (int) $match['y'], $month, $day),
|
||||
];
|
||||
}
|
||||
|
||||
if ($sections === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($sections as $index => &$section) {
|
||||
$section['end'] = $sections[$index + 1]['start'] ?? count($lines);
|
||||
}
|
||||
unset($section);
|
||||
|
||||
$entries = [];
|
||||
foreach ($sections as $section) {
|
||||
for ($i = $section['start']; $i < $section['end']; $i++) {
|
||||
$line = rtrim($lines[$i]);
|
||||
if ($line === '' || !preg_match($referenceRegex, $line, $referenceMatch)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$beforeReference = trim((string) preg_replace($referenceRegex, '', $line));
|
||||
$previousLines = [];
|
||||
for ($k = $i - 1; $k >= $section['start'] && $k >= $i - 15; $k--) {
|
||||
$previous = rtrim($lines[$k]);
|
||||
if ($previous === '' || preg_match($referenceRegex, $previous)) {
|
||||
break;
|
||||
}
|
||||
$previousLines[] = $previous;
|
||||
}
|
||||
|
||||
$verseLines = array_reverse($previousLines);
|
||||
if ($beforeReference !== '') {
|
||||
$verseLines[] = $beforeReference;
|
||||
}
|
||||
|
||||
$verse = trim((string) preg_replace('/\s+/', ' ', implode(' ', $verseLines)));
|
||||
if ($verse !== '') {
|
||||
$entries[] = [
|
||||
'bibeldatum' => $section['date'],
|
||||
'bibelwort' => trim($referenceMatch['ref']),
|
||||
'bibelvers' => $verse,
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$unique = [];
|
||||
$result = [];
|
||||
foreach ($entries as $entry) {
|
||||
$key = $entry['bibeldatum'] . '|' . $entry['bibelwort'];
|
||||
if (!isset($unique[$key])) {
|
||||
$unique[$key] = true;
|
||||
$result[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
function contentpanel_import_leitgedanken(PDO $pdo, array $file): string
|
||||
{
|
||||
$pdfPath = contentpanel_validate_pdf_upload(
|
||||
$file,
|
||||
'Fehler beim Upload der Leitgedanken-PDF.',
|
||||
'Nur PDF-Dateien sind für den Import erlaubt.'
|
||||
);
|
||||
$entries = contentpanel_parse_leitgedanken(contentpanel_pdf_to_text($pdfPath));
|
||||
|
||||
if ($entries === []) {
|
||||
return 'Keine neuen Einträge erkannt.';
|
||||
}
|
||||
|
||||
$exists = $pdo->prepare('SELECT 1 FROM gottesdienst WHERE bibeldatum = ? AND bibelwort = ? LIMIT 1');
|
||||
$insert = $pdo->prepare('INSERT INTO gottesdienst (bibelwort, bibelvers, bibeldatum) VALUES (?, ?, ?)');
|
||||
$created = 0;
|
||||
$duplicates = 0;
|
||||
$ownsTransaction = !$pdo->inTransaction();
|
||||
|
||||
if ($ownsTransaction) {
|
||||
$pdo->beginTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($entries as $entry) {
|
||||
$exists->execute([$entry['bibeldatum'], $entry['bibelwort']]);
|
||||
if ($exists->fetchColumn() !== false) {
|
||||
$duplicates++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$insert->execute([$entry['bibelwort'], $entry['bibelvers'], $entry['bibeldatum']]);
|
||||
$created++;
|
||||
}
|
||||
|
||||
if ($ownsTransaction) {
|
||||
$pdo->commit();
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
if ($ownsTransaction && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
if ($created === 0) {
|
||||
return 'Keine neuen Einträge erkannt.' . ($duplicates > 0 ? " ($duplicates Duplikate)" : '');
|
||||
}
|
||||
|
||||
return "Leitgedanken importiert: $created Einträge angelegt"
|
||||
. ($duplicates > 0 ? ", $duplicates dupliziert übersprungen" : '')
|
||||
. '.';
|
||||
}
|
||||
|
||||
function contentpanel_create_service(PDO $pdo, array $input): string
|
||||
{
|
||||
$date = contentpanel_date($input['bibeldatum'] ?? '', 'Bitte ein gültiges Gottesdienst-Datum angeben.');
|
||||
$reference = contentpanel_required_text($input, 'bibelwort', 'Bitte eine Bibelstelle angeben.');
|
||||
$verse = contentpanel_required_text($input, 'bibelvers', 'Bitte einen Bibelvers angeben.');
|
||||
|
||||
$statement = $pdo->prepare('INSERT INTO gottesdienst (bibelwort, bibelvers, bibeldatum) VALUES (?, ?, ?)');
|
||||
$statement->execute([$reference, $verse, $date->format('Y-m-d')]);
|
||||
|
||||
return 'Gottesdienst am ' . contentpanel_format_message_date($date) . ' erfolgreich angelegt!';
|
||||
}
|
||||
|
||||
function contentpanel_delete_service(PDO $pdo, $id): string
|
||||
{
|
||||
$statement = $pdo->prepare('DELETE FROM gottesdienst WHERE id = ?');
|
||||
$statement->execute([contentpanel_positive_id($id)]);
|
||||
return 'Eintrag erfolgreich gelöscht!';
|
||||
}
|
||||
|
||||
function contentpanel_create_announcement(PDO $pdo, array $input): string
|
||||
{
|
||||
$startDate = contentpanel_date($input['ansage_datum_von'] ?? '', 'Bitte ein gültiges Startdatum angeben.');
|
||||
$endDate = contentpanel_optional_date($input['ansage_datum_bis'] ?? '', 'Bitte ein gültiges Enddatum angeben.');
|
||||
if ($endDate !== null && $endDate < $startDate) {
|
||||
throw new InvalidArgumentException('Das Enddatum darf nicht vor dem Startdatum liegen.');
|
||||
}
|
||||
|
||||
$time = contentpanel_optional_time($input['ansage_uhrzeit'] ?? '', 'Bitte eine gültige Uhrzeit angeben.');
|
||||
$title = contentpanel_required_text($input, 'ansage_titel', 'Bitte einen Titel angeben.');
|
||||
$location = trim((string) ($input['ansage_ort'] ?? ''));
|
||||
$subtitle = trim((string) ($input['ansage_untertitel'] ?? ''));
|
||||
|
||||
$statement = $pdo->prepare(
|
||||
'INSERT INTO ansagen (ansage_datum_von, ansage_datum_bis, ansage_uhrzeit, ansage_ort, ansage_titel, ansage_untertitel)
|
||||
VALUES (?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
$statement->execute([
|
||||
$startDate->format('Y-m-d'),
|
||||
$endDate?->format('Y-m-d'),
|
||||
$time,
|
||||
$location,
|
||||
$title,
|
||||
$subtitle,
|
||||
]);
|
||||
|
||||
return $title . ' am ' . contentpanel_format_message_date($startDate) . ' erfolgreich angelegt!';
|
||||
}
|
||||
|
||||
function contentpanel_delete_announcement(PDO $pdo, $id): string
|
||||
{
|
||||
$statement = $pdo->prepare('DELETE FROM ansagen WHERE id = ?');
|
||||
$statement->execute([contentpanel_positive_id($id)]);
|
||||
return 'Ansage erfolgreich gelöscht!';
|
||||
}
|
||||
|
||||
function contentpanel_sanitize_news_title(string $name): string
|
||||
{
|
||||
$safe = preg_replace('/[^A-Za-z0-9_\-]/', '_', str_replace(' ', '_', $name));
|
||||
return trim((string) $safe, '_-');
|
||||
}
|
||||
|
||||
function contentpanel_is_valid_news_file_base(string $fileBase): bool
|
||||
{
|
||||
return preg_match('/^\d{4}-\d{2}-\d{2}-[A-Za-z0-9_-]+$/D', $fileBase) === 1;
|
||||
}
|
||||
|
||||
function contentpanel_delete_news_files(string $fileBase): bool
|
||||
{
|
||||
if (!contentpanel_is_valid_news_file_base($fileBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$success = true;
|
||||
foreach (glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/' . $fileBase . '_page_*.png') ?: [] as $file) {
|
||||
if (!@unlink($file)) {
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
function contentpanel_cleanup_expired_news(): void
|
||||
{
|
||||
$today = new DateTimeImmutable('today');
|
||||
|
||||
foreach (glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/*_page_1.png') ?: [] as $file) {
|
||||
$fileBase = basename($file, '_page_1.png');
|
||||
if (!contentpanel_is_valid_news_file_base($fileBase)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dateString = substr($fileBase, 0, 10);
|
||||
$expiryDate = DateTimeImmutable::createFromFormat('!Y-m-d', $dateString);
|
||||
if ($expiryDate !== false && $expiryDate->format('Y-m-d') === $dateString && $today > $expiryDate) {
|
||||
contentpanel_delete_news_files($fileBase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function contentpanel_convert_pdf_to_png(string $pdfPath, string $outputDirectory, string $baseFileName): void
|
||||
{
|
||||
$binary = contentpanel_find_binary('gs');
|
||||
if ($binary === null) {
|
||||
throw new RuntimeException("'gs' (Ghostscript) wurde nicht gefunden.");
|
||||
}
|
||||
|
||||
$outputPattern = $outputDirectory . '/' . $baseFileName . '_page_%d.png';
|
||||
$command = escapeshellarg($binary)
|
||||
. ' -dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m -r300'
|
||||
. ' -sOutputFile=' . escapeshellarg($outputPattern)
|
||||
. ' ' . escapeshellarg($pdfPath)
|
||||
. ' 2>&1';
|
||||
|
||||
$output = [];
|
||||
$status = 0;
|
||||
exec($command, $output, $status);
|
||||
if ($status !== 0 || !is_file($outputDirectory . '/' . $baseFileName . '_page_1.png')) {
|
||||
throw new RuntimeException('Ghostscript-Konvertierung fehlgeschlagen: ' . implode("\n", $output));
|
||||
}
|
||||
}
|
||||
|
||||
function contentpanel_upload_news(array $input, array $file): string
|
||||
{
|
||||
$expiryDate = contentpanel_date($input['date'] ?? '', 'Ungültiges Verfallsdatum.');
|
||||
$title = contentpanel_sanitize_news_title(trim((string) ($input['name'] ?? '')));
|
||||
if ($title === '') {
|
||||
throw new InvalidArgumentException('Bitte einen gültigen Titel eingeben.');
|
||||
}
|
||||
|
||||
$pdfPath = contentpanel_validate_pdf_upload(
|
||||
$file,
|
||||
'Bitte eine gültige PDF-Datei hochladen.',
|
||||
'Bitte eine gültige PDF-Datei hochladen.'
|
||||
);
|
||||
|
||||
if (!is_dir(CONTENTPANEL_NEWS_PNG_DIRECTORY) && !mkdir(CONTENTPANEL_NEWS_PNG_DIRECTORY, 0755, true)) {
|
||||
throw new RuntimeException('Das Aushang-Verzeichnis konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
$fileBase = $expiryDate->format('Y-m-d') . '-' . $title;
|
||||
$tempDirectory = sys_get_temp_dir() . '/contentpanel_news_' . bin2hex(random_bytes(8));
|
||||
if (!mkdir($tempDirectory, 0700, true)) {
|
||||
throw new RuntimeException('Temporäres Aushang-Verzeichnis konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
$stagedFiles = [];
|
||||
|
||||
try {
|
||||
contentpanel_convert_pdf_to_png($pdfPath, $tempDirectory, $fileBase);
|
||||
$generatedFiles = glob($tempDirectory . '/' . $fileBase . '_page_*.png') ?: [];
|
||||
natsort($generatedFiles);
|
||||
if ($generatedFiles === []) {
|
||||
throw new RuntimeException('Die PDF-Konvertierung hat keine Seiten erzeugt.');
|
||||
}
|
||||
|
||||
$token = bin2hex(random_bytes(6));
|
||||
foreach ($generatedFiles as $source) {
|
||||
$target = CONTENTPANEL_NEWS_PNG_DIRECTORY . '/' . basename($source);
|
||||
$staged = $target . '.tmp-' . $token;
|
||||
if (!copy($source, $staged)) {
|
||||
throw new RuntimeException('Konvertierte Aushangseite konnte nicht bereitgestellt werden.');
|
||||
}
|
||||
$stagedFiles[$staged] = $target;
|
||||
}
|
||||
|
||||
contentpanel_delete_news_files($fileBase);
|
||||
foreach ($stagedFiles as $staged => $target) {
|
||||
if (!rename($staged, $target)) {
|
||||
throw new RuntimeException('Konvertierte Aushangseite konnte nicht veröffentlicht werden.');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
foreach (glob($tempDirectory . '/*') ?: [] as $filePath) {
|
||||
@unlink($filePath);
|
||||
}
|
||||
@rmdir($tempDirectory);
|
||||
foreach (array_keys($stagedFiles) as $temporaryFile) {
|
||||
@unlink($temporaryFile);
|
||||
}
|
||||
}
|
||||
|
||||
return 'Aushang wurde erfolgreich hochgeladen.';
|
||||
}
|
||||
|
||||
function contentpanel_list_news(): array
|
||||
{
|
||||
$items = [];
|
||||
$files = glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/*_page_1.png') ?: [];
|
||||
sort($files, SORT_STRING);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$fileBase = basename($file, '_page_1.png');
|
||||
if (!contentpanel_is_valid_news_file_base($fileBase)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$expiry = substr($fileBase, 0, 10);
|
||||
$items[] = [
|
||||
'base' => $fileBase,
|
||||
'title' => substr($fileBase, 11),
|
||||
'expiry' => contentpanel_format_display_date($expiry),
|
||||
'pages' => count(glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/' . $fileBase . '_page_*.png') ?: []),
|
||||
'image_url' => '/intranet_repo/projects/contentpanel/insertion/news/png/' . rawurlencode($fileBase) . '_page_1.png',
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
function contentpanel_fetch_services(PDO $pdo, string $today): array
|
||||
{
|
||||
$statement = $pdo->prepare(
|
||||
'SELECT id, bibelwort, bibelvers, bibeldatum
|
||||
FROM gottesdienst
|
||||
WHERE bibeldatum >= ?
|
||||
ORDER BY bibeldatum ASC'
|
||||
);
|
||||
$statement->execute([$today]);
|
||||
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$row['datum'] = contentpanel_format_display_date($row['bibeldatum'] ?? '');
|
||||
}
|
||||
unset($row);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function contentpanel_fetch_announcements(PDO $pdo, string $today): array
|
||||
{
|
||||
$statement = $pdo->prepare(
|
||||
'SELECT id, ansage_untertitel, ansage_titel, ansage_ort, ansage_uhrzeit, ansage_datum_von, ansage_datum_bis
|
||||
FROM ansagen
|
||||
WHERE ansage_datum_von >= ? OR ansage_datum_bis >= ?
|
||||
ORDER BY ansage_datum_von ASC'
|
||||
);
|
||||
$statement->execute([$today, $today]);
|
||||
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$row['datum_von'] = contentpanel_format_display_date($row['ansage_datum_von'] ?? '');
|
||||
$row['datum_bis'] = contentpanel_format_display_date($row['ansage_datum_bis'] ?? '');
|
||||
$row['uhrzeit'] = empty($row['ansage_uhrzeit']) ? '' : substr((string) $row['ansage_uhrzeit'], 0, 5);
|
||||
}
|
||||
unset($row);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
$status = pclose($pipe);
|
||||
if ($status !== 0) {
|
||||
if (isset($userErrors[$status])) {
|
||||
error_log("Contentpanel worker: " . implode("\n", $output));
|
||||
throw new InvalidArgumentException($userErrors[$status]);
|
||||
}
|
||||
throw new RuntimeException($arguments[0] . ' fehlgeschlagen: ' . implode("\n", $output));
|
||||
}
|
||||
return implode("\n", $output);
|
||||
}
|
||||
|
||||
function contentpanel_validate_pdf_upload(array $file, string $uploadError, string $typeError): string
|
||||
{
|
||||
if ((int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
throw new InvalidArgumentException($uploadError);
|
||||
}
|
||||
|
||||
$name = (string) ($file['name'] ?? '');
|
||||
$tmpPath = (string) ($file['tmp_name'] ?? '');
|
||||
if (strtolower(pathinfo($name, PATHINFO_EXTENSION)) !== 'pdf' || !is_file($tmpPath)) {
|
||||
throw new InvalidArgumentException($typeError);
|
||||
}
|
||||
|
||||
$prefix = file_get_contents($tmpPath, false, null, 0, 1024);
|
||||
if ($prefix === false || strpos($prefix, '%PDF-') === false) {
|
||||
throw new InvalidArgumentException($typeError);
|
||||
}
|
||||
|
||||
return $tmpPath;
|
||||
}
|
||||
|
||||
function contentpanel_parse_leitgedanken(string $rawText): array
|
||||
{
|
||||
$text = preg_replace("/\r\n?/", "\n", $rawText);
|
||||
$text = preg_replace("/(?<=\p{L})-\n(?=\p{L})/u", '', (string) $text);
|
||||
$lines = explode("\n", (string) $text);
|
||||
$dateHeaderRegex = '/(?P<prefix>.*?)(?P<d1>\d{1,2})(?:\.\s*\/\s*(?P<d2>\d{1,2}))?\.(?P<m>\d{1,2})\.(?P<y>\d{4})\s*$/u';
|
||||
$books = '(?:Mo|Sam|Kön|Chr|Dan|Ps|Spr|Pred|Hld|Hi|Jes|Jer|Klgl|Hes|Ez|Hos|Joel|Am|Ob|Mi|Nah|Hab|Zef|Hag|Sach|Mal|Rut|Ri|Jos|Esra|Neh|Est|'
|
||||
. 'Mt|Mk|Lk|Joh|Apg|Röm|Kor|Gal|Eph|Phil|Kol|Thess|Tim|Tit|Phlm|Hebr|Jak|Petr|Joh|Jud|Offb)';
|
||||
$referenceRegex = '/(?P<ref>(?:[1-3]\.?\s*)?' . $books . '\s*\d{1,3}\s*,\s*\d{1,3}(?:[–\-]\d{1,3}(?:[a-z])?|(?:\.\d{1,3})+(?:[a-z])?|[a-z])?)\s*$/u';
|
||||
$entries = [];
|
||||
$date = null;
|
||||
$context = [];
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match($dateHeaderRegex, $line, $match)) {
|
||||
$date = checkdate((int) $match['m'], (int) $match['d1'], (int) $match['y'])
|
||||
? sprintf('%04d-%02d-%02d', $match['y'], $match['m'], $match['d1']) : null;
|
||||
$context = [];
|
||||
} elseif ($date !== null && preg_match($referenceRegex, $line, $match)) {
|
||||
$verse = trim(preg_replace('/\s+/', ' ', implode(' ', $context) . ' '
|
||||
. preg_replace($referenceRegex, '', $line)));
|
||||
if ($verse !== '') {
|
||||
$entries[$date . '|' . $match['ref']] ??= [
|
||||
'bibeldatum' => $date, 'bibelwort' => $match['ref'], 'bibelvers' => $verse,
|
||||
];
|
||||
}
|
||||
$date = null;
|
||||
} else {
|
||||
$context = trim($line) === '' ? [] : array_slice([...$context, trim($line)], -15);
|
||||
}
|
||||
}
|
||||
return array_values($entries);
|
||||
}
|
||||
|
||||
|
||||
function contentpanel_import_leitgedanken(PDO $pdo, array $file): string
|
||||
{
|
||||
$pdfPath = contentpanel_validate_pdf_upload(
|
||||
$file,
|
||||
'Fehler beim Upload der Leitgedanken-PDF.',
|
||||
'Nur PDF-Dateien sind für den Import erlaubt.'
|
||||
);
|
||||
$entries = contentpanel_parse_leitgedanken(contentpanel_run(['pdftotext', '-layout', '-enc', 'UTF-8', $pdfPath, '-']));
|
||||
if ($entries === []) {
|
||||
return 'Keine neuen Einträge erkannt.';
|
||||
}
|
||||
|
||||
$exists = $pdo->prepare('SELECT 1 FROM gottesdienst WHERE bibeldatum = ? AND bibelwort = ? LIMIT 1');
|
||||
$insert = $pdo->prepare('INSERT INTO gottesdienst (bibelwort, bibelvers, bibeldatum) VALUES (?, ?, ?)');
|
||||
$created = 0;
|
||||
$duplicates = 0;
|
||||
$ownsTransaction = !$pdo->inTransaction();
|
||||
if ($ownsTransaction) {
|
||||
$pdo->beginTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($entries as $entry) {
|
||||
$exists->execute([$entry['bibeldatum'], $entry['bibelwort']]);
|
||||
if ($exists->fetchColumn() !== false) {
|
||||
$duplicates++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$insert->execute([$entry['bibelwort'], $entry['bibelvers'], $entry['bibeldatum']]);
|
||||
$created++;
|
||||
}
|
||||
|
||||
if ($ownsTransaction) {
|
||||
$pdo->commit();
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
if ($ownsTransaction && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
if ($created === 0) {
|
||||
return 'Keine neuen Einträge erkannt.' . ($duplicates > 0 ? " ($duplicates Duplikate)" : '');
|
||||
}
|
||||
|
||||
return "Leitgedanken importiert: $created Einträge angelegt"
|
||||
. ($duplicates > 0 ? ", $duplicates dupliziert übersprungen" : '')
|
||||
. '.';
|
||||
}
|
||||
|
||||
function contentpanel_create_service(PDO $pdo, array $input): string
|
||||
{
|
||||
$date = contentpanel_date($input['bibeldatum'] ?? '', 'Bitte ein gültiges Gottesdienst-Datum angeben.');
|
||||
$reference = contentpanel_required_text($input, 'bibelwort', 'Bitte eine Bibelstelle angeben.');
|
||||
$verse = contentpanel_required_text($input, 'bibelvers', 'Bitte einen Bibelvers angeben.');
|
||||
contentpanel_query($pdo, 'INSERT INTO gottesdienst (bibelwort, bibelvers, bibeldatum) VALUES (?, ?, ?)',
|
||||
[$reference, $verse, $date->format('Y-m-d')]);
|
||||
return 'Gottesdienst am ' . contentpanel_format_message_date($date) . ' erfolgreich angelegt!';
|
||||
}
|
||||
|
||||
function contentpanel_create_announcement(PDO $pdo, array $input): string
|
||||
{
|
||||
$startDate = contentpanel_date($input['ansage_datum_von'] ?? '', 'Bitte ein gültiges Startdatum angeben.');
|
||||
$endDate = contentpanel_optional_date($input['ansage_datum_bis'] ?? '', 'Bitte ein gültiges Enddatum angeben.');
|
||||
if ($endDate !== null && $endDate < $startDate) {
|
||||
throw new InvalidArgumentException('Das Enddatum darf nicht vor dem Startdatum liegen.');
|
||||
}
|
||||
|
||||
$time = contentpanel_optional_time($input['ansage_uhrzeit'] ?? '', 'Bitte eine gültige Uhrzeit angeben.');
|
||||
$title = contentpanel_required_text($input, 'ansage_titel', 'Bitte einen Titel angeben.');
|
||||
$location = trim((string) ($input['ansage_ort'] ?? ''));
|
||||
$subtitle = trim((string) ($input['ansage_untertitel'] ?? ''));
|
||||
contentpanel_query($pdo,
|
||||
'INSERT INTO ansagen (ansage_datum_von, ansage_datum_bis, ansage_uhrzeit, ansage_ort, ansage_titel, ansage_untertitel)
|
||||
VALUES (?, ?, ?, ?, ?, ?)', [
|
||||
$startDate->format('Y-m-d'),
|
||||
$endDate?->format('Y-m-d'),
|
||||
$time,
|
||||
$location, $title, $subtitle,
|
||||
]);
|
||||
return $title . ' am ' . contentpanel_format_message_date($startDate) . ' erfolgreich angelegt!';
|
||||
}
|
||||
|
||||
function contentpanel_delete_entry(PDO $pdo, string $action, $id): string
|
||||
{
|
||||
$table = match ($action) {
|
||||
'delete_service' => 'gottesdienst',
|
||||
'delete_announcement' => 'ansagen',
|
||||
};
|
||||
contentpanel_query($pdo, "DELETE FROM $table WHERE id = ?", [contentpanel_positive_id($id)]);
|
||||
return 'Eintrag erfolgreich gelöscht!';
|
||||
}
|
||||
|
||||
function contentpanel_query(PDO $pdo, string $sql, array $params): PDOStatement
|
||||
{
|
||||
$statement = $pdo->prepare($sql);
|
||||
$statement->execute($params);
|
||||
return $statement;
|
||||
}
|
||||
|
||||
function contentpanel_fetch_services(PDO $pdo, string $today): array
|
||||
{
|
||||
return contentpanel_query($pdo,
|
||||
'SELECT id, bibelwort AS title, bibelvers AS subtitle, bibeldatum AS date
|
||||
FROM gottesdienst WHERE bibeldatum >= ? ORDER BY bibeldatum', [$today])->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
function contentpanel_fetch_announcements(PDO $pdo, string $today): array
|
||||
{
|
||||
return contentpanel_query($pdo,
|
||||
'SELECT id, ansage_titel AS title, ansage_untertitel AS subtitle, ansage_datum_von AS date,
|
||||
ansage_datum_bis AS end_date, ansage_uhrzeit AS time, ansage_ort AS location
|
||||
FROM ansagen WHERE ansage_datum_von >= ? OR ansage_datum_bis >= ? ORDER BY ansage_datum_von',
|
||||
[$today, $today])->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
const CONTENTPANEL_NEWS_PNG_DIRECTORY = __DIR__ . '/../news/png';
|
||||
const CONTENTPANEL_AUSHANG_TEMPLATE = __DIR__ . '/../template/aushang-template.docx';
|
||||
const CONTENTPANEL_AUSHANG_WORKER = '/opt/intranet/worker/contentpanel_aushang/process.py';
|
||||
const CONTENTPANEL_PYTHON = '/opt/intranet-venv/bin/python';
|
||||
|
||||
function contentpanel_sanitize_news_title(string $name): string
|
||||
{
|
||||
return trim(preg_replace('/[^A-Za-z0-9_-]/', '_', $name), '_-');
|
||||
}
|
||||
|
||||
function contentpanel_is_valid_news_file_base(string $fileBase): bool
|
||||
{
|
||||
return preg_match('/^\d{4}-\d{2}-\d{2}-[A-Za-z0-9_-]+$/D', $fileBase) === 1;
|
||||
}
|
||||
|
||||
function contentpanel_delete_news_files(string $fileBase): bool
|
||||
{
|
||||
if (!contentpanel_is_valid_news_file_base($fileBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return contentpanel_news_locked(function () use ($fileBase) {
|
||||
$success = true;
|
||||
foreach (glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/' . $fileBase . '_page_*.png') ?: [] as $file) {
|
||||
$success = @unlink($file) && $success;
|
||||
}
|
||||
return $success;
|
||||
});
|
||||
}
|
||||
|
||||
function contentpanel_news_locked(callable $operation)
|
||||
{
|
||||
$lock = fopen(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/.publish.lock', 'c');
|
||||
if (!$lock) throw new RuntimeException('Aushang-Verzeichnis konnte nicht gesperrt werden.');
|
||||
try {
|
||||
if (!flock($lock, LOCK_EX)) throw new RuntimeException('Aushang-Verzeichnis konnte nicht gesperrt werden.');
|
||||
return $operation();
|
||||
} finally {
|
||||
fclose($lock);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function contentpanel_cleanup_expired_news(): void
|
||||
{
|
||||
$today = new DateTimeImmutable('today');
|
||||
foreach (glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/*_page_1.png') ?: [] as $file) {
|
||||
$fileBase = basename($file, '_page_1.png');
|
||||
if (!contentpanel_is_valid_news_file_base($fileBase)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dateString = substr($fileBase, 0, 10);
|
||||
$expiryDate = DateTimeImmutable::createFromFormat('!Y-m-d', $dateString);
|
||||
if ($expiryDate !== false && $expiryDate->format('Y-m-d') === $dateString && $today > $expiryDate) {
|
||||
contentpanel_delete_news_files($fileBase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function contentpanel_render_news(
|
||||
string $pdfPath, string $outputDirectory, string $fileBase, string $mode, ?string $date = null
|
||||
): array {
|
||||
$timeout = $mode === 'template' ? filter_var(getenv('CONTENTPANEL_JOB_TIMEOUT_SECONDS') ?: '3600', FILTER_VALIDATE_INT,
|
||||
['options' => ['min_range' => 60, 'max_range' => 86400]]) : 600;
|
||||
if ($timeout === false) throw new RuntimeException('Ungültiges Verarbeitungstimeout.');
|
||||
$output = contentpanel_run([CONTENTPANEL_PYTHON, CONTENTPANEL_AUSHANG_WORKER,
|
||||
'--pdf', $pdfPath, '--template', CONTENTPANEL_AUSHANG_TEMPLATE,
|
||||
'--output-dir', $outputDirectory, '--base-name', $fileBase,
|
||||
'--date', $date ?? date('Y-m-d'), '--mode', $mode], $timeout, [
|
||||
2 => 'Die KI-Ausgabe enthält eine nicht belegte Zahl oder Kontaktangabe. Bitte erneut versuchen oder die Original-PDF-Darstellung verwenden.',
|
||||
3 => 'Die KI-Verarbeitung ist fehlgeschlagen. Für Bild-PDFs muss das konfigurierte Modell Bilder lesen können. Bitte erneut versuchen oder die Original-PDF-Darstellung verwenden.',
|
||||
4 => 'Die PDF konnte nicht vollständig gelesen werden. Bitte prüfen Sie die Datei und ihre Lesbarkeit.',
|
||||
124 => 'Die PDF-Verarbeitung hat zu lange gedauert. Bitte verwenden Sie eine kleinere PDF oder die Original-PDF-Darstellung.',
|
||||
]);
|
||||
$result = json_decode($output, true, 32, JSON_THROW_ON_ERROR);
|
||||
if (($result['ok'] ?? false) !== true || !is_int($result['pages'] ?? null) || $result['pages'] < 1) {
|
||||
throw new RuntimeException('Der Aushang-Worker meldet ein unvollständiges Ergebnis.');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
function contentpanel_publish_news_pages(array $generatedFiles, string $fileBase): void
|
||||
{
|
||||
if (!$generatedFiles || !contentpanel_is_valid_news_file_base($fileBase)) {
|
||||
throw new RuntimeException('Es wurden keine gültigen Aushangseiten erzeugt.');
|
||||
}
|
||||
foreach (array_values($generatedFiles) as $index => $source) {
|
||||
if (!is_file($source) || basename($source) !== $fileBase . '_page_' . ($index + 1) . '.png') {
|
||||
throw new RuntimeException('Die erzeugten Aushangseiten sind unvollständig.');
|
||||
}
|
||||
}
|
||||
contentpanel_news_locked(function () use ($generatedFiles, $fileBase) {
|
||||
$token = bin2hex(random_bytes(6));
|
||||
$backups = $published = [];
|
||||
try {
|
||||
foreach (glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/' . $fileBase . '_page_*.png') ?: [] as $existing) {
|
||||
$backup = $existing . '.bak-' . $token;
|
||||
if (!rename($existing, $backup)) throw new RuntimeException('Aushang konnte nicht gesichert werden.');
|
||||
$backups[$backup] = $existing;
|
||||
}
|
||||
foreach ($generatedFiles as $source) {
|
||||
$target = CONTENTPANEL_NEWS_PNG_DIRECTORY . '/' . basename($source);
|
||||
if (!rename($source, $target)) throw new RuntimeException('Aushangseite konnte nicht veröffentlicht werden.');
|
||||
$published[] = $target;
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
foreach ($published as $file) @unlink($file);
|
||||
$restored = true;
|
||||
foreach ($backups as $backup => $original) $restored = @rename($backup, $original) && $restored;
|
||||
if (!$restored) {
|
||||
// Failed backups are retained as the last copy of the original announcement.
|
||||
throw new RuntimeException('Veröffentlichung fehlgeschlagen. Sicherungsdateien konnten nicht vollständig zurückgespielt werden.', 0, $exception);
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
foreach ($backups as $backup => $original) @unlink($backup);
|
||||
});
|
||||
}
|
||||
|
||||
function contentpanel_upload_news(array $input, array $file, PDO $pdo, array $user): string
|
||||
{
|
||||
$expiryDate = contentpanel_date($input['date'] ?? '', 'Ungültiges Verfallsdatum.');
|
||||
$title = contentpanel_sanitize_news_title(trim((string) ($input['name'] ?? '')));
|
||||
if ($title === '') {
|
||||
throw new InvalidArgumentException('Bitte einen gültigen Titel eingeben.');
|
||||
}
|
||||
|
||||
$mode = trim((string) ($input['format_mode'] ?? 'original'));
|
||||
if (!in_array($mode, ['original', 'template'], true)) {
|
||||
throw new InvalidArgumentException('Ungültiger Aushang-Modus.');
|
||||
}
|
||||
|
||||
$pdfPath = contentpanel_validate_pdf_upload(
|
||||
$file,
|
||||
'Bitte eine gültige PDF-Datei hochladen.',
|
||||
'Bitte eine gültige PDF-Datei hochladen.'
|
||||
);
|
||||
$job = ['base' => $expiryDate->format('Y-m-d') . '-' . $title, 'title' => trim((string) $input['name']),
|
||||
'mode' => $mode, 'date' => date('Y-m-d')];
|
||||
if ($mode === 'template') {
|
||||
return contentpanel_enqueue_news($pdo, $user, $pdfPath, $job);
|
||||
}
|
||||
contentpanel_process_news($pdfPath, $job);
|
||||
return 'Aushang wurde veröffentlicht.';
|
||||
}
|
||||
|
||||
function contentpanel_process_news(string $pdfPath, array $job, ?callable $beforePublish = null): array
|
||||
{
|
||||
if (!is_dir(CONTENTPANEL_NEWS_PNG_DIRECTORY) && !mkdir(CONTENTPANEL_NEWS_PNG_DIRECTORY, 0755, true)) {
|
||||
throw new RuntimeException('Das Aushang-Verzeichnis konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
$fileBase = $job['base'];
|
||||
// Same filesystem as the final images: publishing only needs atomic renames.
|
||||
$tempDirectory = CONTENTPANEL_NEWS_PNG_DIRECTORY . '/.upload-' . (isset($job['id']) ? 'job-' . (int) $job['id'] : bin2hex(random_bytes(8)));
|
||||
if (!is_dir($tempDirectory) && !mkdir($tempDirectory, 0700, true)) {
|
||||
throw new RuntimeException('Temporäres Aushang-Verzeichnis konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
try {
|
||||
// A restarted queue job may have left incomplete render output.
|
||||
foreach (glob($tempDirectory . '/*') ?: [] as $path) if (is_file($path)) unlink($path);
|
||||
$result = contentpanel_render_news($pdfPath, $tempDirectory, $fileBase, $job['mode'], $job['date']);
|
||||
$generatedFiles = glob($tempDirectory . '/' . $fileBase . '_page_*.png') ?: [];
|
||||
natsort($generatedFiles);
|
||||
if (count($generatedFiles) !== $result['pages']) {
|
||||
throw new RuntimeException('Die erzeugte Seitenzahl ist unvollständig.');
|
||||
}
|
||||
if ($beforePublish) $beforePublish();
|
||||
contentpanel_publish_news_pages($generatedFiles, $fileBase);
|
||||
|
||||
return $result;
|
||||
} finally {
|
||||
foreach (glob($tempDirectory . '/*') ?: [] as $filePath) {
|
||||
if (is_file($filePath)) {
|
||||
@unlink($filePath);
|
||||
}
|
||||
}
|
||||
@rmdir($tempDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
function contentpanel_list_news(): array
|
||||
{
|
||||
$items = [];
|
||||
foreach (glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/*_page_1.png') ?: [] as $file) {
|
||||
$base = basename($file, '_page_1.png');
|
||||
if (!is_file($file) || !contentpanel_is_valid_news_file_base($base)) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'base' => $base,
|
||||
'title' => substr($base, 11),
|
||||
'pages' => count(glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/' . $base . '_page_[0-9]*.png') ?: []),
|
||||
'expiry' => contentpanel_format_display_date(substr($base, 0, 10)),
|
||||
'image_url' => '/intranet_repo/projects/contentpanel/insertion/news/png/'
|
||||
. rawurlencode(basename($file)) . '?v=' . (int) filemtime($file),
|
||||
];
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
function contentpanel_queue_schema(PDO $pdo): void
|
||||
{
|
||||
// Separate from the worker lock: uploads never wait for an active conversion.
|
||||
$pdo->query('SELECT pg_advisory_lock(742001, 0)');
|
||||
try {
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS contentpanel_jobs (
|
||||
id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
payload JSONB NOT NULL, pdf BYTEA, attempts SMALLINT NOT NULL DEFAULT 0,
|
||||
result JSONB, mailed_at TIMESTAMPTZ, retry_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)");
|
||||
} finally {
|
||||
$pdo->query('SELECT pg_advisory_unlock(742001, 0)');
|
||||
}
|
||||
}
|
||||
|
||||
function contentpanel_enqueue_news(PDO $pdo, array $user, string $pdf, array $job): string
|
||||
{
|
||||
if (!filter_var($user['mail'] ?? '', FILTER_VALIDATE_EMAIL)) {
|
||||
throw new InvalidArgumentException('Bitte zuerst eine gültige E-Mail-Adresse im Benutzerkonto hinterlegen.');
|
||||
}
|
||||
contentpanel_queue_schema($pdo);
|
||||
$job['mail'] = $user['mail'];
|
||||
$job['author'] = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? '')) ?: $user['mail'];
|
||||
$statement = $pdo->prepare("INSERT INTO contentpanel_jobs (payload, pdf) VALUES (?, decode(?, 'base64'))");
|
||||
$statement->execute([json_encode($job, JSON_THROW_ON_ERROR), base64_encode(file_get_contents($pdf))]);
|
||||
return 'Die Datei wird im Hintergrund von unserem Server verarbeitet. Sie erhalten eine E-Mail, sobald die Verarbeitung abgeschlossen ist.';
|
||||
}
|
||||
|
||||
function contentpanel_notify_job(PDO $pdo, ?int $id = null): void
|
||||
{
|
||||
$statement = $pdo->prepare('SELECT id, payload, result FROM contentpanel_jobs
|
||||
WHERE result IS NOT NULL AND mailed_at IS NULL AND retry_at <= NOW()'
|
||||
. ($id === null ? ' ORDER BY id LIMIT 1' : ' AND id = ?'));
|
||||
$statement->execute($id === null ? [] : [$id]);
|
||||
if (!$row = $statement->fetch()) return;
|
||||
$job = json_decode($row['payload'], true, 512, JSON_THROW_ON_ERROR);
|
||||
$result = json_decode($row['result'], true, 512, JSON_THROW_ON_ERROR);
|
||||
$subject = 'Aushang ' . ($result['ok'] ? 'veröffentlicht: ' : 'fehlgeschlagen: ') . $job['title'];
|
||||
$message = isset($result['title']) ? "{$result['title']}\n\n{$result['message']}" : "{$result['message']}";
|
||||
try {
|
||||
$sent = send_intranet_mail($job['mail'], $subject, $message);
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Contentpanel mail ' . $row['id'] . ': ' . $exception->getMessage());
|
||||
$sent = false;
|
||||
}
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$pdo->prepare("UPDATE contentpanel_jobs SET mailed_at = CASE WHEN ? = 1 THEN NOW() ELSE NULL END,
|
||||
retry_at = NOW() + INTERVAL '5 minutes' WHERE id = ?")->execute([(int) $sent, $row['id']]);
|
||||
$pdo->prepare('UPDATE admin_messages SET message = ? WHERE id = ?')->execute([
|
||||
$message . "\nHochgeladen von: {$job['author']} ({$job['mail']})\nE-Mail: "
|
||||
. ($sent ? 'an den Mailserver übergeben.' : 'Versand fehlgeschlagen; wird erneut versucht.'), $result['admin_id']]);
|
||||
$pdo->commit();
|
||||
} catch (Throwable $exception) {
|
||||
$pdo->rollBack();
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
function contentpanel_queue_once(PDO $pdo): bool
|
||||
{
|
||||
if (!(int) $pdo->query('SELECT pg_try_advisory_lock(742001, 1)::int')->fetchColumn()) return false;
|
||||
try {
|
||||
contentpanel_notify_job($pdo);
|
||||
$row = $pdo->query("SELECT id, payload, attempts, encode(pdf, 'base64') AS pdf
|
||||
FROM contentpanel_jobs WHERE result IS NULL ORDER BY id LIMIT 1")->fetch();
|
||||
if (!$row) {
|
||||
$pdo->exec("DELETE FROM contentpanel_jobs WHERE mailed_at < NOW() - INTERVAL '7 days'");
|
||||
return false;
|
||||
}
|
||||
$job = json_decode($row['payload'], true, 512, JSON_THROW_ON_ERROR);
|
||||
$job['id'] = (int) $row['id'];
|
||||
$pdo->prepare('UPDATE contentpanel_jobs SET attempts = attempts + 1 WHERE id = ?')->execute([$job['id']]);
|
||||
$pdf = tmpfile();
|
||||
try {
|
||||
if (!$pdf) throw new RuntimeException('Temporäre PDF-Datei konnte nicht angelegt werden.');
|
||||
if (substr($job['base'], 0, 10) < date('Y-m-d')) throw new InvalidArgumentException('Das Verfallsdatum ist bereits überschritten. Bitte mit einem neuen Datum hochladen.');
|
||||
$bytes = base64_decode($row['pdf'], true);
|
||||
if ($bytes === false || fwrite($pdf, $bytes) !== strlen($bytes)) throw new RuntimeException('Die gespeicherte PDF konnte nicht vollständig gelesen werden.');
|
||||
$beforePublish = static fn() => $pdo->query('SELECT 1'); // Lost connection also means lost worker ownership.
|
||||
try {
|
||||
if ($row['attempts'] >= 2) throw new RuntimeException('Die Formatüberführung wurde mehrfach unterbrochen.');
|
||||
$render = contentpanel_process_news(stream_get_meta_data($pdf)['uri'], $job, $beforePublish);
|
||||
$result = ['ok' => true, 'title' => $render['title'], 'message' => 'Der Aushang wurde erfolgreich in unsere Formatvorlage überführt und veröffentlicht.'];
|
||||
} catch (Throwable $conversion) {
|
||||
error_log('Contentpanel fallback ' . $job['id'] . ': ' . $conversion->getMessage());
|
||||
$pdo->query('SELECT 1');
|
||||
contentpanel_process_news(stream_get_meta_data($pdf)['uri'], array_replace($job, ['mode' => 'original']), $beforePublish);
|
||||
$result = ['ok' => true, 'fallback' => true,
|
||||
'message' => 'Die Formatüberführung ist fehlgeschlagen. Stattdessen wurde die Original-PDF unverändert als Aushang veröffentlicht.'];
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Contentpanel job ' . $job['id'] . ': ' . $exception->getMessage());
|
||||
$result = ['ok' => false, 'message' => $exception instanceof InvalidArgumentException
|
||||
? $exception->getMessage() : 'Der Aushang konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut oder wenden Sie sich an die Administration.'];
|
||||
} finally {
|
||||
if (is_resource($pdf)) fclose($pdf);
|
||||
}
|
||||
// Completion and the inbox entry commit together; mail retries never rerun the PDF.
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$statement = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at)
|
||||
VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW()) RETURNING id");
|
||||
$statement->execute([$job['author'], $job['mail'], 'Aushang ' . ($result['ok'] ? 'veröffentlicht: ' : 'fehlgeschlagen: ') . $job['title'],
|
||||
"Auftrag #{$job['id']}: {$job['title']}\n{$result['message']}\nE-Mail: Versand ausstehend."]);
|
||||
$result['admin_id'] = (int) $statement->fetchColumn();
|
||||
$pdo->prepare('UPDATE contentpanel_jobs SET result = ?, pdf = NULL WHERE id = ?')
|
||||
->execute([json_encode($result, JSON_THROW_ON_ERROR), $job['id']]);
|
||||
$pdo->commit();
|
||||
} catch (Throwable $exception) {
|
||||
$pdo->rollBack();
|
||||
throw $exception;
|
||||
}
|
||||
contentpanel_notify_job($pdo, $job['id']);
|
||||
return true;
|
||||
} finally {
|
||||
$pdo->query('SELECT pg_advisory_unlock(742001, 1)');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
// Shared markup for the repeated fields and POST metadata; all values are escaped.
|
||||
function contentpanel_field(string $name, string $label, array $attributes = [], bool $full = false): void
|
||||
{
|
||||
$attributes += ['type' => 'text', 'id' => $name, 'name' => $name, 'class' => 'sb-field'];
|
||||
$required = !empty($attributes['required']);
|
||||
?>
|
||||
<div class="sb-field-group<?= $full ? ' sb-form-full' : '' ?>">
|
||||
<label class="sb-label" for="<?= contentpanel_html($attributes['id']) ?>">
|
||||
<?= contentpanel_html($label) ?>
|
||||
<span class="<?= $required ? 'sb-required' : 'sb-optional' ?>"><?= $required ? '*' : '(optional)' ?></span>
|
||||
</label>
|
||||
<input<?php foreach ($attributes as $key => $value): ?>
|
||||
<?php if ($value !== false): ?> <?= contentpanel_html($key) ?>="<?= contentpanel_html($value === true ? $key : $value) ?>"<?php endif; ?>
|
||||
<?php endforeach; ?>>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
function contentpanel_hidden(string $action, array $values = []): void
|
||||
{
|
||||
foreach (['csrf_token' => contentpanel_csrf_token(), 'action' => $action] + $values as $name => $value) {
|
||||
echo '<input type="hidden" name="' . contentpanel_html($name) . '" value="' . contentpanel_html($value) . '">';
|
||||
}
|
||||
}
|
||||
|
||||
function contentpanel_flash(?array $flash, string $tab): void
|
||||
{
|
||||
if (($flash['tab'] ?? '') === $tab && !empty($flash['message'])) {
|
||||
echo '<div role="status" class="sb-alert' . (($flash['type'] ?? '') === 'error' ? ' sb-alert-error' : '')
|
||||
. '">' . contentpanel_html($flash['message']) . '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function contentpanel_entries(array $entries, string $action): void
|
||||
{
|
||||
?>
|
||||
<div class="sb-entry-list">
|
||||
<?php if (!$entries): ?><div class="sb-empty">Keine Einträge vorhanden.</div><?php endif; ?>
|
||||
<?php foreach ($entries as $entry): ?>
|
||||
<?php
|
||||
$date = contentpanel_format_display_date($entry['date']);
|
||||
if (!empty($entry['end_date'])) $date .= ' – ' . contentpanel_format_display_date($entry['end_date']);
|
||||
$meta = implode(' · ', array_filter([
|
||||
empty($entry['time']) ? '' : substr($entry['time'], 0, 5) . ' Uhr', $entry['location'] ?? '',
|
||||
]));
|
||||
?>
|
||||
<div class="sb-entry">
|
||||
<div class="sb-entry-info">
|
||||
<div class="sb-entry-date"><?= contentpanel_html($date) ?></div>
|
||||
<div class="sb-entry-title"><?= contentpanel_html($entry['title']) ?></div>
|
||||
<?php if (!empty($entry['subtitle'])): ?><div class="sb-entry-subtitle"><?= contentpanel_html($entry['subtitle']) ?></div><?php endif; ?>
|
||||
<?php if ($meta !== ''): ?><div class="sb-entry-meta"><?= contentpanel_html($meta) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<form method="post" action="insert.php">
|
||||
<?php contentpanel_hidden($action, ['id' => $entry['id']]); ?>
|
||||
<button type="submit" class="sb-btn sb-btn-danger" data-confirm="Eintrag wirklich löschen?">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
Reference in New Issue
Block a user