Compare commits

...

19 Commits

Author SHA1 Message Date
60eea92f11 feat(wiki): Beitragsliste für Betriebsrat-Nutzer auf zugängliche Kategorien filtern
BR-Nutzer (r-wiki-br ohne r-wiki) sehen in der Beitragsliste und im
Kategorie-Dropdown nur Beiträge und Kategorien, die ihren freigegebenen
Kategorien (betriebsrat_editable=1 + alle Nachfahren per BFS) zugeordnet sind.

Der Filter wird server-seitig in post_listform.php berechnet und als Parameter
an den Ajax-Endpunkt übergeben, da get_permission_state() im Ajax-Kontext
nicht verfügbar ist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 10:07:48 +02:00
5da7916651 fix(wiki): Speichern-Button für Betriebsrat-Wiki-Nutzer sichtbar machen
Copy-paste-Fehler: Berechtigung wurde doppelt gegen r-wiki geprüft statt
gegen r-wiki-br, wodurch BR-Nutzer den Speichern-Button nie sahen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 10:07:32 +02:00
8aadf65cc3 feat(einricht): Verknüpfte Einrichtungen bei Mehrfachauswahl automatisch mitselektieren
Wenn eine Einrichtung mit hinterlegten Links ausgewählt wird, werden alle
verknüpften Einrichtungen (aus organigramm_einricht_link) automatisch
mitausgewählt. Gilt für alle 5 Einrichtungs-Multiselects (3× Semantic UI
Dropdown, 2× Tagify).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 10:07:25 +02:00
ab4f38a360 feat(einricht): Einrichtungen miteinander verknüpfen
Neue Tabelle organigramm_einricht_link für n:m-Beziehungen zwischen
Einrichtungen. In der Bearbeitungskarte gibt es jetzt ein
Mehrfachauswahlfeld (Semantic UI Dropdown) um verwandte Einrichtungen
zu verknüpfen (z.B. alle HzE-Einrichtungen unter der Gruppe HzE).

- Neue Funktion saveLinkedEinrichts() speichert die Verknüpfungen
- Cardform lädt bestehende Links als Vorauswahl (Strings, kein int-Cast
  damit Semantic UI den data-value-Vergleich korrekt auflöst)
- onChange-Handler + explizite hidden-input-Befüllung sichern
  zuverlässige Übergabe beim Formular-Submit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 08:46:11 +02:00
61dde2925b chore: Filesgallery-Cache-Images aus Git ausschließen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 08:46:00 +02:00
3aa523870f fix(wiki): Betriebsrat-UI vollständig korrigiert
- categories_posts_listform.php: brCategoryFilter entfernt, Betriebsrat
  sieht alle Kategorien (nur "Beitrag erstellen"-Button gefiltert)
- post_cardform_article.php: Bearbeiten- und "Neue Version"-Button jetzt
  über \$canEditPost gesteuert statt hart auf r-wiki geprüft
- post_cardform.php: Settings-Tab-Link und -Inhalt ebenfalls über
  \$canEditPost freigegeben
- post_cardform_settings.php: Kategorie-Whitelist wird für Betriebsrat
  auf freigegebene Kategorien gefiltert (inkl. Vererbung);
  Speichern-Button und JS-Handler über \$canEditPost gesteuert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 08:09:19 +02:00
4d6cc807c9 feat(wiki): Kategorienvererbung für betriebsrat_editable, Ajax-Absicherung
- is_category_betriebsrat_editable(): läuft den Kategoriebaum nach oben durch
  (via knowledgecenter_category_links), sodass Unterkategorien einer
  freigegebenen Kategorie automatisch ebenfalls als editierbar gelten
- ajax_is_category_editable_for_br(): gleiche Logik für den Ajax-Kontext
- ajax_can_edit_post(): nutzt jetzt Vererbung statt direkter Kategorienprüfung
- create_post_draft(): ajax_has_wiki_edit()-Check hinzugefügt
- update_post_version(): Permission-Check mit Sonderfall neue Drafts (noch
  keine Kategorien → Prüfung auf has_wiki_edit begrenzt)
- update_post_categories(): Betriebsrat darf nur freigegebene Kategorien
  (inkl. Vererbung) zuweisen
- upload_inline_image.php: Bild-Upload prüft ebenfalls den Kategoriebaum

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 08:08:59 +02:00
a0698d621e feat(wiki): Betriebsrat-Berechtigung (r-wiki-br)
Neue Permission r-wiki-br ermöglicht eingeschränkte Wiki-Bearbeitung:

- Neue Permission in main_permission (ID 30: r-wiki-br)
- Neue Spalte betriebsrat_editable in knowledgecenter_categories_update
- Checkbox "Für Betriebsrat bearbeitbar" in der Kategorie-Verwaltung
  (nur für r-wiki-Redakteure sichtbar und speicherbar)

Berechtigungslogik:
- r-wiki schlägt immer: Wer beide Rechte hat, verhält sich wie Redakteur
- Betriebsrat sieht nur Kategorien mit betriebsrat_editable=1
- "Beitrag erstellen"-Button nur in freigegebenen Kategorien sichtbar
- Neue Posts/Bearbeitungen auf betriebsrat_editable-Kategorien beschränkt
- Settings-Tab und Kategorieverwaltung weiterhin r-wiki-only
- Ajax-Endpoints (save_post_files, delete_post_file, upload_inline_image)
  prüfen Kategorie-Berechtigung serverseitig

File Gallery:
- Betriebsrat ist per kc_lock_path auf den Post-eigenen Ordner beschränkt
- Redakteur kann frei navigieren (kein Lock)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 13:16:36 +02:00
07f9f304a4 fix(wiki-migration): pdftotext als primären PDF-Extraktor nutzen
pdfparser scheitert an "secured" PDFs (Kopier-Schutz ohne Passwort).
pdftotext aus poppler-utils umgeht diesen Schutz und extrahiert den
eingebetteten Text korrekt. Falls pdftotext nicht verfügbar ist oder
nichts liefert, bleibt pdfparser als Fallback erhalten.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 13:40:47 +02:00
9817fde8c4 fix(wiki-migration): PDF-Extraktion über alle filegallery-Sitepart-Typen
get_article_files() filterte nur sitepart_id=5, obwohl PDFs auch über
sitepart_id=6 und 10 verknüpft sind. Direkter JOIN auf filegallery_line
mit IN (5,6,10) findet jetzt alle Anhänge korrekt.

Außerdem: Token-sparsamere generate_summary-Logik (Teaser priorisiert,
Text auf 1500 Z. gecappt, nur 1 PDF á 800 Z., maxOutputTokens=150),
--summaries-only-Flag für Nachläufe, sleep(4) für Free-Tier-Rate-Limit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 13:33:56 +02:00
0bf47247ff chore: Weiterleitungs-Icons, Mobile-Nav-Fix, Aufräumen
- Forwarding: neue Icons Glocke und Einstellungen, SVG-Syntax korrigiert
- global.css: Hint-Badge in mobiler Navigation korrekt positioniert
- text_constants.inc.php entfernt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 10:00:33 +02:00
54d9194d22 refactor(quill): Toolbar-Presets als Factory-Funktionen, Resize-Plugin überarbeitet
Toolbar-Presets liefern jetzt frische Arrays (Factory-Pattern), damit
mehrere Editor-Instanzen auf einer Seite sich nicht gegenseitig
beeinflussen. Resize-Modul und CSS angepasst.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 10:00:25 +02:00
8ff44a407e feat(wiki): Kategoriebeschreibungen, Kacheldesign und Migration
- Migrationsskript: Teaser/Content getrennt (kc_teaser id=55 → teaser,
  kc_article id=58 → content), Gemini-Summaries mit thinkingBudget=0,
  Retry-Logik für 503/429-Fehler
- Berechtigungsfehler behoben: AND active=1 aus main_contact_department-
  Abfrage entfernt (Middleware + Ajax)
- Kategoriebeschreibungen: neues description-Feld in
  knowledgecenter_category_translations, aus kc.tags befüllt
- Kacheln: Hauptebene erhält post_wiki-Klasse für einheitliches Design,
  Beschreibungstext in Grid-Ansicht, in Listenansicht ausgeblendet
- Buttons: Beitrag-erstellen nutzt --clr-mandant, alle vier Toolbar-
  Buttons auf gleiche Höhe normiert (line-height: 1 + SVG 1em)
- Platzhalterpfade für Kategorien und Beiträge korrigiert
- Zusammenfassung in Beitragsansicht angezeigt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 10:00:17 +02:00
ee966de174 Fix: Aufgaben-Badge in Navigation wurde außerhalb des <a>-Tags ausgegeben
echo \$my_tasks_dot stand vor dem <li>, sodass der rote Badge als
freies Element zwischen den Navigationspunkten erschien. Badge-Span
wird jetzt innerhalb des <a>-Tags gerendert (analog zu \$collection_dot),
und \$my_tasks_dot wird zu Beginn jeder Loop-Iteration auf "" gesetzt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 14:08:38 +02:00
55e6d4c583 Fix: Tasks-Endpoints ignorierten die Monolith-Session (sidawo)
bootstrap.php rief session_start() mit Standard-Name PHPSESSID auf,
weil start.inc.php für Endpoint-Requests bewusst nicht geladen wird.
Das führte zu einer leeren Session → contactId = 0 → "Keine Berechtigung".

Lösung: Vor session_start() prüfen ob ein sid*-Cookie vorhanden ist
und ggf. session_name() entsprechend setzen, damit Endpoints und
Monolith dieselbe Session nutzen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 13:56:17 +02:00
cf97a52128 Fix: Aufgaben-Formular (neu anlegen) zeigte User-Detail statt Admin-Formular
Sitepart 68 ("Bearbeitungsmaske") hatte den falschen Typ 36
(user/task_detail.php). Dadurch wurde beim Öffnen der "neue Aufgabe"-
Seite der User-Task-Detail-Action aufgerufen, die ohne assignment_id
mit "Ungültige Assignment-ID." abbricht.

Lösung: Typ 40 in intranet.php für admin/task_form.php eingetragen,
Sitepart 68 in der DB auf Typ 40 gesetzt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 13:52:05 +02:00
833c3718cf Add summary field to knowledgecenter posts
- ALTER TABLE knowledgecenter_post_versions ADD COLUMN summary TEXT NULL AFTER content
- Add summary textarea to post edit form (after Quill editor)
- Include summary in insert_post_version() and update_post_version()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 11:59:31 +02:00
952b3e2efc Remove topic table query from post_cardform.php
The forum module (topic table) does not exist. Remove the comment count
query to prevent a fatal crash when loading a post detail page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 11:52:42 +02:00
887ac6a4bb Fix post save and file gallery for knowledgecenter
- Create module/shared_components/quill/ with toolbar_presets.js,
  quill_resize.js and quill_resize.css — missing files caused a JS crash
  in $(document).ready() before the save button's click handler was
  registered, so clicking save did nothing
- Fix filesgallery.php session detection to search any sid* cookie
  dynamically (site session name is 'sidawo', not the hardcoded
  'sidintranet'), fixing the popup showing the homepage instead of
  the gallery
- Revert categories_cardform.php from 3-tab layout back to original
  sidebar layout with full-width permissions section below

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 11:49:59 +02:00
31 changed files with 2137 additions and 420 deletions

6
.gitignore vendored
View File

@@ -23,4 +23,8 @@ docker/
*.csv *.csv
*.png *.png
.DS_Store .DS_Store
# Filesgallery-Cache (generierte Thumbnails)
module/knowledgecenter_update/Plugins/_files/cache/images/*
!module/knowledgecenter_update/Plugins/_files/cache/images/.gitkeep

View File

@@ -558,6 +558,10 @@ span.hint {
flex-shrink: 0; flex-shrink: 0;
} }
nav span.hint {
left: -0;
}
.profil_banner.certificate { .profil_banner.certificate {
width: 33.33%; width: 33.33%;
float: left; float: left;
@@ -9187,6 +9191,13 @@ a:not([class]) {
text-decoration-skip-ink: auto; text-decoration-skip-ink: auto;
} }
.mobile_navigation_container .hint {
position: absolute;
right: 15px;
top: -5px;
left: unset;
}
.mobile_navigation svg { .mobile_navigation svg {
width: 19px; width: 19px;
height: 19px; height: 19px;

View File

@@ -2893,18 +2893,50 @@ function multi_select_permission($collection_id, $type){
<script type="text/javascript" src="/plugins/jquery/jquery-3.5.1.min.js"></script> <script type="text/javascript" src="/plugins/jquery/jquery-3.5.1.min.js"></script>
<script type="text/javascript" src="/plugins/Semantic-UI-master/semantic.min.js"></script> <script type="text/javascript" src="/plugins/Semantic-UI-master/semantic.min.js"></script>
<link rel="stylesheet" type="text/css" href="/plugins/Semantic-UI-master/bc-semantic.css?time=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/plugins/Semantic-UI-master/bc-semantic.css')?>"> <link rel="stylesheet" type="text/css" href="/plugins/Semantic-UI-master/bc-semantic.css?time=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/plugins/Semantic-UI-master/bc-semantic.css')?>">
<?php
$einrichtLinks = [];
if ($type == "einricht") {
$linkRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT einricht_id, linked_einricht_id FROM organigramm_einricht_link");
while ($lr = mysqli_fetch_assoc($linkRes)) {
$einrichtLinks[strval($lr['einricht_id'])][] = strval($lr['linked_einricht_id']);
}
}
?>
<script> <script>
var $y = jQuery.noConflict(); var $y = jQuery.noConflict();
$y( document ).ready(function() { $y( document ).ready(function() {
$y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>').dropdown('set selected', <?= json_encode($preselect) ?>); var $drop = $y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>');
<?php if ($type == "einricht"): ?>
var einrichtLinks = <?= json_encode($einrichtLinks) ?>;
var busy = false;
$drop.dropdown({
onChange: function(value) {
if (busy) return;
var current = value ? value.split(',').filter(Boolean) : [];
var toAdd = [];
current.forEach(function(id) {
(einrichtLinks[id] || []).forEach(function(lid) {
if (current.indexOf(lid) < 0 && toAdd.indexOf(lid) < 0) toAdd.push(lid);
});
});
if (toAdd.length) {
busy = true;
$drop.dropdown('set selected', current.concat(toAdd));
busy = false;
}
}
});
<?php endif; ?>
$drop.dropdown('set selected', <?= json_encode($preselect) ?>);
}); });
$("<?php echo "#all_get".$type;?>").click(function() { $("<?php echo "#all_get".$type;?>").click(function() {
$y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>').dropdown('set selected', <?= json_encode($allselect) ?>); $y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>').dropdown('set selected', <?= json_encode($allselect) ?>);
}); });
$("<?php echo "#all_del".$type;?>").click(function() { $("<?php echo "#all_del".$type;?>").click(function() {
console.log("delete"); console.log("delete");
$y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>').dropdown('clear'); $y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>').dropdown('clear');
}); });
</script> </script>
<? <?

View File

@@ -94,20 +94,24 @@ $forwarding_icon_images = array(
<path id="Pfad_280" data-name="Pfad 280" d="M18,11a7,7,0,1,1-7,7,7,7,0,0,1,7-7"/> <path id="Pfad_280" data-name="Pfad 280" d="M18,11a7,7,0,1,1-7,7,7,7,0,0,1,7-7"/>
<path id="Pfad_281" data-name="Pfad 281" d="M18,34A16,16,0,1,1,34,18,16,16,0,0,1,18,34ZM18,4A14,14,0,1,0,32,18,14,14,0,0,0,18,4Z"/> <path id="Pfad_281" data-name="Pfad 281" d="M18,34A16,16,0,1,1,34,18,16,16,0,0,1,18,34ZM18,4A14,14,0,1,0,32,18,14,14,0,0,0,18,4Z"/>
</g> </g>
</svg> </svg>'
'
), ),
"communityboard" => array( "communityboard" => array(
"name" => "Communityboard", "name" => "Communityboard",
"image" => '<svg xmlns="https://www.w3.org/2000/svg" width="18.98" height="14.914" viewBox="0 0 18.98 14.914"> "image" => '<svg xmlns="https://www.w3.org/2000/svg" width="18.98" height="14.914" viewBox="0 0 18.98 14.914">
<path id="comments-o" d="M7.457,1.356a8.244,8.244,0,0,0-3.03.551A5.677,5.677,0,0,0,2.187,3.4a3.01,3.01,0,0,0-.831,2.023A2.886,2.886,0,0,0,1.917,7.1,4.875,4.875,0,0,0,3.5,8.495l1.028.593-.371.89q.36-.212.657-.413l.466-.328.562.106a9.171,9.171,0,0,0,1.621.148,8.244,8.244,0,0,0,3.03-.551,5.677,5.677,0,0,0,2.24-1.494,3.01,3.01,0,0,0,.831-2.023A3.01,3.01,0,0,0,12.727,3.4a5.676,5.676,0,0,0-2.241-1.494,8.249,8.249,0,0,0-3.03-.551ZM7.457,0A9.543,9.543,0,0,1,11.2.725,6.719,6.719,0,0,1,13.918,2.7a4.22,4.22,0,0,1,0,5.444A6.728,6.728,0,0,1,11.2,10.121a9.533,9.533,0,0,1-3.744.725,10.654,10.654,0,0,1-1.864-.169,8.9,8.9,0,0,1-2.945,1.356,8.823,8.823,0,0,1-.911.169H1.705a.331.331,0,0,1-.217-.085.35.35,0,0,1-.122-.222.217.217,0,0,1-.011-.069.421.421,0,0,1,.005-.069.23.23,0,0,1,.021-.064l.026-.053.037-.058.042-.053.047-.053.042-.047q.053-.064.244-.265a4.008,4.008,0,0,0,.275-.313q.084-.112.238-.307a2.62,2.62,0,0,0,.265-.408q.111-.212.217-.466A6.2,6.2,0,0,1,.752,7.8,4.155,4.155,0,0,1,0,5.423,4.279,4.279,0,0,1,1,2.7,6.723,6.723,0,0,1,3.712.725,9.543,9.543,0,0,1,7.457,0Zm8.706,12.382q.106.254.217.466a2.672,2.672,0,0,0,.265.408q.153.2.238.307t.275.313q.19.2.244.265l.042.047q.032.037.047.053a.449.449,0,0,1,.042.053.475.475,0,0,1,.037.058l.026.053.021.064.005.069-.011.069a.392.392,0,0,1-.138.233.319.319,0,0,1-.233.074,9.054,9.054,0,0,1-.911-.169,8.916,8.916,0,0,1-2.945-1.356,10.654,10.654,0,0,1-1.864.169,8.9,8.9,0,0,1-5-1.4q.614.042.932.042a11.192,11.192,0,0,0,3.273-.477,9.368,9.368,0,0,0,2.8-1.367,7.086,7.086,0,0,0,2.034-2.246,5.451,5.451,0,0,0,.466-4.3A6.268,6.268,0,0,1,18.185,5.7a4.134,4.134,0,0,1,.043,4.814A6.221,6.221,0,0,1,16.163,12.382Z"/> <path id="comments-o" d="M7.457,1.356a8.244,8.244,0,0,0-3.03.551A5.677,5.677,0,0,0,2.187,3.4a3.01,3.01,0,0,0-.831,2.023A2.886,2.886,0,0,0,1.917,7.1,4.875,4.875,0,0,0,3.5,8.495l1.028.593-.371.89q.36-.212.657-.413l.466-.328.562.106a9.171,9.171,0,0,0,1.621.148,8.244,8.244,0,0,0,3.03-.551,5.677,5.677,0,0,0,2.24-1.494,3.01,3.01,0,0,0,.831-2.023A3.01,3.01,0,0,0,12.727,3.4a5.676,5.676,0,0,0-2.241-1.494,8.249,8.249,0,0,0-3.03-.551ZM7.457,0A9.543,9.543,0,0,1,11.2.725,6.719,6.719,0,0,1,13.918,2.7a4.22,4.22,0,0,1,0,5.444A6.728,6.728,0,0,1,11.2,10.121a9.533,9.533,0,0,1-3.744.725,10.654,10.654,0,0,1-1.864-.169,8.9,8.9,0,0,1-2.945,1.356,8.823,8.823,0,0,1-.911.169H1.705a.331.331,0,0,1-.217-.085.35.35,0,0,1-.122-.222.217.217,0,0,1-.011-.069.421.421,0,0,1,.005-.069.23.23,0,0,1,.021-.064l.026-.053.037-.058.042-.053.047-.053.042-.047q.053-.064.244-.265a4.008,4.008,0,0,0,.275-.313q.084-.112.238-.307a2.62,2.62,0,0,0,.265-.408q.111-.212.217-.466A6.2,6.2,0,0,1,.752,7.8,4.155,4.155,0,0,1,0,5.423,4.279,4.279,0,0,1,1,2.7,6.723,6.723,0,0,1,3.712.725,9.543,9.543,0,0,1,7.457,0Zm8.706,12.382q.106.254.217.466a2.672,2.672,0,0,0,.265.408q.153.2.238.307t.275.313q.19.2.244.265l.042.047q.032.037.047.053a.449.449,0,0,1,.042.053.475.475,0,0,1,.037.058l.026.053.021.064.005.069-.011.069a.392.392,0,0,1-.138.233.319.319,0,0,1-.233.074,9.054,9.054,0,0,1-.911-.169,8.916,8.916,0,0,1-2.945-1.356,10.654,10.654,0,0,1-1.864.169,8.9,8.9,0,0,1-5-1.4q.614.042.932.042a11.192,11.192,0,0,0,3.273-.477,9.368,9.368,0,0,0,2.8-1.367,7.086,7.086,0,0,0,2.034-2.246,5.451,5.451,0,0,0,.466-4.3A6.268,6.268,0,0,1,18.185,5.7a4.134,4.134,0,0,1,.043,4.814A6.221,6.221,0,0,1,16.163,12.382Z"/>
</svg> </svg>'
'
), ),
"glocke" => array(
"name" => "Glocke",
"image" => '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bell" viewBox="0 0 16 16">
<path d="M8 16a2 2 0 0 0 2-2H6a2 2 0 0 0 2 2M8 1.918l-.797.161A4 4 0 0 0 4 6c0 .628-.134 2.197-.459 3.742-.16.767-.376 1.566-.663 2.258h10.244c-.287-.692-.502-1.49-.663-2.258C12.134 8.197 12 6.628 12 6a4 4 0 0 0-3.203-3.92zM14.22 12c.223.447.481.801.78 1H1c.299-.199.557-.553.78-1C2.68 10.2 3 6.88 3 6c0-2.42 1.72-4.44 4.005-4.901a1 1 0 1 1 1.99 0A5 5 0 0 1 13 6c0 .88.32 4.2 1.22 6"/>
</svg>'
),
"einstellungen" => array(
"name" => "Einstellungen",
"image" => '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-sliders" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.5 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3M9.05 3a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0V3zM4.5 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3M2.05 8a2.5 2.5 0 0 1 4.9 0H16v1H6.95a2.5 2.5 0 0 1-4.9 0H0V8zm9.45 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3m-2.45 1a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0v-1z"/>
</svg>'
)
); );

View File

@@ -124,6 +124,9 @@ function intranet_show($sitepart_id) {
case 39: case 39:
$include = MODULE_PATH.'/tasks/pages/user/escalation_overlay_widget.php'; $include = MODULE_PATH.'/tasks/pages/user/escalation_overlay_widget.php';
break; break;
case 40:
$include = MODULE_PATH.'/tasks/pages/admin/task_form.php';
break;
default: default:
break; break;
} }

View File

@@ -572,18 +572,50 @@ function multi_select_permission($collection_id, $type){
<script type="text/javascript" src="/plugins/jquery/jquery-3.5.1.min.js"></script> <script type="text/javascript" src="/plugins/jquery/jquery-3.5.1.min.js"></script>
<script type="text/javascript" src="/plugins/Semantic-UI-master/semantic.min.js"></script> <script type="text/javascript" src="/plugins/Semantic-UI-master/semantic.min.js"></script>
<link rel="stylesheet" type="text/css" href="/plugins/Semantic-UI-master/bc-semantic.css?time=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/plugins/Semantic-UI-master/bc-semantic.css')?>"> <link rel="stylesheet" type="text/css" href="/plugins/Semantic-UI-master/bc-semantic.css?time=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/plugins/Semantic-UI-master/bc-semantic.css')?>">
<?php
$einrichtLinks = [];
if ($type == "einricht") {
$linkRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT einricht_id, linked_einricht_id FROM organigramm_einricht_link");
while ($lr = mysqli_fetch_assoc($linkRes)) {
$einrichtLinks[strval($lr['einricht_id'])][] = strval($lr['linked_einricht_id']);
}
}
?>
<script> <script>
var $y = jQuery.noConflict(); var $y = jQuery.noConflict();
$y( document ).ready(function() { $y( document ).ready(function() {
$y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>').dropdown('set selected', <?= json_encode($preselect) ?>); var $drop = $y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>');
<?php if ($type == "einricht"): ?>
var einrichtLinks = <?= json_encode($einrichtLinks) ?>;
var busy = false;
$drop.dropdown({
onChange: function(value) {
if (busy) return;
var current = value ? value.split(',').filter(Boolean) : [];
var toAdd = [];
current.forEach(function(id) {
(einrichtLinks[id] || []).forEach(function(lid) {
if (current.indexOf(lid) < 0 && toAdd.indexOf(lid) < 0) toAdd.push(lid);
});
});
if (toAdd.length) {
busy = true;
$drop.dropdown('set selected', current.concat(toAdd));
busy = false;
}
}
});
<?php endif; ?>
$drop.dropdown('set selected', <?= json_encode($preselect) ?>);
}); });
$("<?php echo "#all_get".$type;?>").click(function() { $("<?php echo "#all_get".$type;?>").click(function() {
$y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>').dropdown('set selected', <?= json_encode($allselect) ?>); $y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>').dropdown('set selected', <?= json_encode($allselect) ?>);
}); });
$("<?php echo "#all_del".$type;?>").click(function() { $("<?php echo "#all_del".$type;?>").click(function() {
console.log("delete"); console.log("delete");
$y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>').dropdown('clear'); $y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>').dropdown('clear');
}); });
</script> </script>
<? <?

View File

@@ -24,6 +24,100 @@ require_once($connEnv);
db_connect(); db_connect();
// Prüft ob der aktuelle User Wiki-Schreibrechte hat (r-wiki oder r-wiki-br)
function ajax_has_wiki_edit(): bool
{
$mid = $GLOBALS['main_contact']['master_mandant_id'] ?? 0;
$uid = $GLOBALS['main_contact']['id'] ?? 0;
return get_permission_state('r-wiki', $mid, $uid) == 1
|| get_permission_state('r-wiki-br', $mid, $uid) == 1;
}
// Prüft ob eine Kategorie (oder ein Vorfahre) betriebsrat_editable=1 hat.
function ajax_is_category_editable_for_br(int $categoryId): bool
{
$visited = [];
$queue = [$categoryId];
while (!empty($queue)) {
$current = array_shift($queue);
if (isset($visited[$current])) {
continue;
}
$visited[$current] = true;
$res = mysqli_query($GLOBALS['mysql_con'],
"SELECT betriebsrat_editable FROM knowledgecenter_categories_update WHERE id = $current LIMIT 1");
$row = mysqli_fetch_assoc($res);
if ($row && (int)$row['betriebsrat_editable'] === 1) {
return true;
}
$pRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT parent_category_id FROM knowledgecenter_category_links WHERE child_category_id = $current");
while ($pRow = mysqli_fetch_assoc($pRes)) {
$pid = (int)$pRow['parent_category_id'];
if (!isset($visited[$pid])) {
$queue[] = $pid;
}
}
}
return false;
}
// Alle Kategorie-IDs, die für Betriebsrat zugänglich sind (direkt betriebsrat_editable=1
// plus alle Nachfahren im Baum BFS nach unten).
function ajax_get_all_betriebsrat_accessible_category_ids(): array
{
$seeds = [];
$res = mysqli_query($GLOBALS['mysql_con'],
"SELECT id FROM knowledgecenter_categories_update WHERE betriebsrat_editable = 1");
while ($row = mysqli_fetch_assoc($res)) {
$seeds[] = (int)$row['id'];
}
if (empty($seeds)) {
return [];
}
$visited = [];
$queue = $seeds;
while (!empty($queue)) {
$current = array_shift($queue);
if (isset($visited[$current])) {
continue;
}
$visited[$current] = true;
$cRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT child_category_id FROM knowledgecenter_category_links WHERE parent_category_id = $current");
while ($cRow = mysqli_fetch_assoc($cRes)) {
$cid = (int)$cRow['child_category_id'];
if (!isset($visited[$cid])) {
$queue[] = $cid;
}
}
}
return array_keys($visited);
}
// Betriebsrat darf einen Post nur bearbeiten, wenn er in einer freigegebenen
// Kategorie (oder Unterkategorie einer freigegebenen Kategorie) liegt.
function ajax_can_edit_post(int $postId): bool
{
$mid = $GLOBALS['main_contact']['master_mandant_id'] ?? 0;
$uid = $GLOBALS['main_contact']['id'] ?? 0;
if (get_permission_state('r-wiki', $mid, $uid) == 1) {
return true;
}
if (get_permission_state('r-wiki-br', $mid, $uid) != 1) {
return false;
}
// Betriebsrat: prüfe alle dem Post zugeordneten Kategorien inkl. Vorfahren
$res = mysqli_query($GLOBALS['mysql_con'],
"SELECT category_id FROM knowledgecenter_category_post WHERE post_id = $postId");
while ($row = mysqli_fetch_assoc($res)) {
if (ajax_is_category_editable_for_br((int)$row['category_id'])) {
return true;
}
}
return false;
}
// AJAX-Handler: Falls per POST eine Aktion übergeben wurde, diese verarbeiten und das Skript beenden. // AJAX-Handler: Falls per POST eine Aktion übergeben wurde, diese verarbeiten und das Skript beenden.
if (isset($_POST["action"])) { if (isset($_POST["action"])) {
header('Content-Type: application/json'); header('Content-Type: application/json');
@@ -243,6 +337,7 @@ function update_category_detail()
{ {
$is_navigation_item = isset($_POST['is_navigation_item']) ? (int) $_POST['is_navigation_item'] : 0; $is_navigation_item = isset($_POST['is_navigation_item']) ? (int) $_POST['is_navigation_item'] : 0;
$enable_post_color_pickers = isset($_POST['enable_post_color_pickers']) ? (int) $_POST['enable_post_color_pickers'] : 0; $enable_post_color_pickers = isset($_POST['enable_post_color_pickers']) ? (int) $_POST['enable_post_color_pickers'] : 0;
$betriebsrat_editable = isset($_POST['betriebsrat_editable']) ? (int) $_POST['betriebsrat_editable'] : 0;
$color_hex = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['color_hex'] ?? ''); $color_hex = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['color_hex'] ?? '');
$categoryId = (int) ($_POST['id'] ?? 0); $categoryId = (int) ($_POST['id'] ?? 0);
$languageId = (int) ($_POST['language_id'] ?? 0); $languageId = (int) ($_POST['language_id'] ?? 0);
@@ -287,11 +382,12 @@ function update_category_detail()
exit; exit;
} }
$mainUpdate = "UPDATE knowledgecenter_categories_update $mainUpdate = "UPDATE knowledgecenter_categories_update
SET modified_by = '$modified_by', SET modified_by = '$modified_by',
modified_at = NOW(), modified_at = NOW(),
state = $state, state = $state,
enable_post_color_pickers = $enable_post_color_pickers, enable_post_color_pickers = $enable_post_color_pickers,
betriebsrat_editable = $betriebsrat_editable,
color_hex = '$color_hex' color_hex = '$color_hex'
WHERE id = $categoryId"; WHERE id = $categoryId";
if (!mysqli_query($GLOBALS['mysql_con'], $mainUpdate)) { if (!mysqli_query($GLOBALS['mysql_con'], $mainUpdate)) {
@@ -686,6 +782,22 @@ function update_post_version()
echo json_encode(['error' => 'Ungültige Beitrags-ID']); echo json_encode(['error' => 'Ungültige Beitrags-ID']);
exit; exit;
} }
if (!ajax_has_wiki_edit()) {
echo json_encode(['error' => 'Keine Berechtigung']);
exit;
}
// Betriebsrat darf bestehende Posts nur bearbeiten wenn der Post in einer freigegebenen Kategorie liegt.
// Neue Drafts haben noch keine Kategorien → Prüfung überspringen (Zugang wurde schon bei create_post_draft gesichert).
if ($postId > 0) {
$catCountRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT COUNT(*) AS cnt FROM knowledgecenter_category_post WHERE post_id = $postId");
$catCountRow = mysqli_fetch_assoc($catCountRes);
if ((int)$catCountRow['cnt'] > 0 && !ajax_can_edit_post($postId)) {
echo json_encode(['error' => 'Keine Berechtigung für diesen Beitrag']);
exit;
}
}
$keepModifiedAt = isset($_POST['keep_modified_at']) && (int) $_POST['keep_modified_at'] === 1; $keepModifiedAt = isset($_POST['keep_modified_at']) && (int) $_POST['keep_modified_at'] === 1;
$backgroundColor = normalize_hex_color($_POST['background_color'] ?? '#FFFFFF', '#FFFFFF'); $backgroundColor = normalize_hex_color($_POST['background_color'] ?? '#FFFFFF', '#FFFFFF');
@@ -707,6 +819,7 @@ function update_post_version()
$image = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['image'][$langId] ?? ''); $image = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['image'][$langId] ?? '');
$teaser = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['teaser'][$langId] ?? ''); $teaser = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['teaser'][$langId] ?? '');
$content = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['content'][$langId] ?? ''); $content = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['content'][$langId] ?? '');
$summary = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['summary'][$langId] ?? '');
$author_id = isset($_POST['author_id'][$langId]) ? (int) $_POST['author_id'][$langId] : 0; $author_id = isset($_POST['author_id'][$langId]) ? (int) $_POST['author_id'][$langId] : 0;
$link = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['link'][$langId] ?? ''); $link = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['link'][$langId] ?? '');
$link_label = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['link_label'][$langId] ?? ''); $link_label = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['link_label'][$langId] ?? '');
@@ -726,8 +839,8 @@ function update_post_version()
$versionId = isset($_POST['version_id'][$langId]) ? (int) $_POST['version_id'][$langId] : 0; $versionId = isset($_POST['version_id'][$langId]) ? (int) $_POST['version_id'][$langId] : 0;
if ($versionId <= 0) { if ($versionId <= 0) {
// INSERT: Ermittle die neue Versionnummer… // INSERT: Ermittle die neue Versionnummer…
$query = "SELECT MAX(version_number) AS max_ver $query = "SELECT MAX(version_number) AS max_ver
FROM knowledgecenter_post_versions FROM knowledgecenter_post_versions
WHERE post_id = $postId AND language_id = $langId"; WHERE post_id = $postId AND language_id = $langId";
$result = mysqli_query($GLOBALS['mysql_con'], $query); $result = mysqli_query($GLOBALS['mysql_con'], $query);
$row = mysqli_fetch_assoc($result); $row = mysqli_fetch_assoc($result);
@@ -752,12 +865,12 @@ function update_post_version()
$insertQuery = " $insertQuery = "
INSERT INTO knowledgecenter_post_versions INSERT INTO knowledgecenter_post_versions
(post_id, language_id, version_number, (post_id, language_id, version_number,
title, image, link, link_label, teaser, content, author_id, state, title, image, link, link_label, teaser, content, summary, author_id, state,
modified_at, valid_from, valid_until, background_color, text_color) modified_at, valid_from, valid_until, background_color, text_color)
VALUES VALUES
($postId, $langId, $newVersion, ($postId, $langId, $newVersion,
'$title', '$image', '$link', '$link_label', '$teaser', '$content', $author_id, $state, '$title', '$image', '$link', '$link_label', '$teaser', '$content', '$summary', $author_id, $state,
$insertModifiedAtSql, $valid_from_sql, $valid_until_sql, '$backgroundColor', '$textColor') $insertModifiedAtSql, $valid_from_sql, $valid_until_sql, '$backgroundColor', '$textColor')
"; ";
if (!mysqli_query($GLOBALS['mysql_con'], $insertQuery)) { if (!mysqli_query($GLOBALS['mysql_con'], $insertQuery)) {
echo json_encode(['error' => 'Fehler beim Einfügen für Sprache ' . $langId . ': ' . mysqli_error($GLOBALS['mysql_con'])]); echo json_encode(['error' => 'Fehler beim Einfügen für Sprache ' . $langId . ': ' . mysqli_error($GLOBALS['mysql_con'])]);
@@ -767,20 +880,21 @@ function update_post_version()
} }
// UPDATE: Aktualisiere auch die Datumsfelder // UPDATE: Aktualisiere auch die Datumsfelder
$query = " $query = "
UPDATE knowledgecenter_post_versions UPDATE knowledgecenter_post_versions
SET title = '$title', SET title = '$title',
image = '$image', image = '$image',
link = '$link', link = '$link',
link_label = '$link_label', link_label = '$link_label',
teaser = '$teaser', teaser = '$teaser',
content = '$content', content = '$content',
author_id = $author_id, summary = '$summary',
state = $state, author_id = $author_id,
valid_from = $valid_from_sql, state = $state,
valid_until = $valid_until_sql, valid_from = $valid_from_sql,
background_color = '$backgroundColor', valid_until = $valid_until_sql,
text_color = '$textColor'" . ($keepModifiedAt ? "" : ",\n modified_at = NOW()") . " background_color = '$backgroundColor',
text_color = '$textColor'" . ($keepModifiedAt ? "" : ",\n modified_at = NOW()") . "
WHERE id = $versionId AND language_id = $langId WHERE id = $versionId AND language_id = $langId
"; ";
if (!mysqli_query($GLOBALS['mysql_con'], $query)) { if (!mysqli_query($GLOBALS['mysql_con'], $query)) {
@@ -855,6 +969,7 @@ function insert_post_version()
$image = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['image'][$langId] ?? ''); $image = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['image'][$langId] ?? '');
$teaser = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['teaser'][$langId] ?? ''); $teaser = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['teaser'][$langId] ?? '');
$content = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['content'][$langId] ?? ''); $content = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['content'][$langId] ?? '');
$summary = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['summary'][$langId] ?? '');
$link = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['link'][$langId] ?? ''); $link = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['link'][$langId] ?? '');
$link_label = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['link_label'][$langId] ?? ''); $link_label = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['link_label'][$langId] ?? '');
$author_id = isset($_POST['author_id'][$langId]) ? (int) $_POST['author_id'][$langId] : 0; $author_id = isset($_POST['author_id'][$langId]) ? (int) $_POST['author_id'][$langId] : 0;
@@ -867,8 +982,8 @@ function insert_post_version()
$valid_from_sql = ($valid_from !== 'NULL' && !empty($valid_from)) ? "'$valid_from'" : "NULL"; $valid_from_sql = ($valid_from !== 'NULL' && !empty($valid_from)) ? "'$valid_from'" : "NULL";
$valid_until_sql = ($valid_until !== 'NULL' && !empty($valid_until)) ? "'$valid_until'" : "NULL"; $valid_until_sql = ($valid_until !== 'NULL' && !empty($valid_until)) ? "'$valid_until'" : "NULL";
$query = "SELECT MAX(version_number) AS max_ver $query = "SELECT MAX(version_number) AS max_ver
FROM knowledgecenter_post_versions FROM knowledgecenter_post_versions
WHERE post_id = $postId AND language_id = $langId"; WHERE post_id = $postId AND language_id = $langId";
$result = mysqli_query($GLOBALS['mysql_con'], $query); $result = mysqli_query($GLOBALS['mysql_con'], $query);
$row = mysqli_fetch_assoc($result); $row = mysqli_fetch_assoc($result);
@@ -890,9 +1005,9 @@ function insert_post_version()
} }
} }
$insertQuery = "INSERT INTO knowledgecenter_post_versions $insertQuery = "INSERT INTO knowledgecenter_post_versions
(post_id, language_id, version_number, title, image, link, link_label, teaser, content, author_id, state, modified_at, valid_from, valid_until, background_color, text_color) (post_id, language_id, version_number, title, image, link, link_label, teaser, content, summary, author_id, state, modified_at, valid_from, valid_until, background_color, text_color)
VALUES ($postId, $langId, $newVersion, '$title', '$image', '$link', '$link_label', '$teaser', '$content', $author_id, $state, $insertModifiedAtSql, $valid_from_sql, $valid_until_sql, '$backgroundColor', '$textColor')"; VALUES ($postId, $langId, $newVersion, '$title', '$image', '$link', '$link_label', '$teaser', '$content', '$summary', $author_id, $state, $insertModifiedAtSql, $valid_from_sql, $valid_until_sql, '$backgroundColor', '$textColor')";
if (!mysqli_query($GLOBALS['mysql_con'], $insertQuery)) { if (!mysqli_query($GLOBALS['mysql_con'], $insertQuery)) {
echo json_encode(['error' => "Fehler beim Einfügen für Sprache $langId: " . mysqli_error($GLOBALS['mysql_con'])]); echo json_encode(['error' => "Fehler beim Einfügen für Sprache $langId: " . mysqli_error($GLOBALS['mysql_con'])]);
exit; exit;
@@ -1026,6 +1141,10 @@ function delete_post_gallery_dir($postId)
function create_post_draft() function create_post_draft()
{ {
if (!ajax_has_wiki_edit()) {
echo json_encode(['error' => 'Keine Berechtigung']);
exit;
}
$insertPostQuery = "INSERT INTO knowledgecenter_posts (created_at) VALUES (NOW())"; $insertPostQuery = "INSERT INTO knowledgecenter_posts (created_at) VALUES (NOW())";
if (!mysqli_query($GLOBALS['mysql_con'], $insertPostQuery)) { if (!mysqli_query($GLOBALS['mysql_con'], $insertPostQuery)) {
echo json_encode(['error' => 'Fehler beim Erstellen des Entwurfs: ' . mysqli_error($GLOBALS['mysql_con'])]); echo json_encode(['error' => 'Fehler beim Erstellen des Entwurfs: ' . mysqli_error($GLOBALS['mysql_con'])]);
@@ -1240,6 +1359,27 @@ function update_post_categories()
exit; exit;
} }
$mid = $GLOBALS['main_contact']['master_mandant_id'] ?? 0;
$uid = $GLOBALS['main_contact']['id'] ?? 0;
$isRedakteur = get_permission_state('r-wiki', $mid, $uid) == 1;
$isBetriebsrat = get_permission_state('r-wiki-br', $mid, $uid) == 1;
if (!$isRedakteur && !$isBetriebsrat) {
echo json_encode(['error' => 'Keine Berechtigung']);
exit;
}
// Betriebsrat darf nur Kategorien zuweisen, die für ihn freigegeben sind (inkl. Vererbung)
if (!$isRedakteur && $isBetriebsrat) {
foreach ($categories as $catId) {
$catId = (int)$catId;
if ($catId > 0 && !ajax_is_category_editable_for_br($catId)) {
echo json_encode(['error' => "Kategorie $catId ist nicht für den Betriebsrat freigegeben"]);
exit;
}
}
}
// Bestehende Einträge für diesen Beitrag löschen // Bestehende Einträge für diesen Beitrag löschen
$deleteQuery = "DELETE FROM knowledgecenter_category_post WHERE post_id = $postId"; $deleteQuery = "DELETE FROM knowledgecenter_category_post WHERE post_id = $postId";
if (!mysqli_query($GLOBALS['mysql_con'], $deleteQuery)) { if (!mysqli_query($GLOBALS['mysql_con'], $deleteQuery)) {
@@ -1312,7 +1452,7 @@ function get_post_list()
// nur Posts, die in dieser Kategorie sind // nur Posts, die in dieser Kategorie sind
$categoryCondition = " $categoryCondition = "
AND EXISTS ( AND EXISTS (
SELECT 1 SELECT 1
FROM knowledgecenter_category_post AS cp FROM knowledgecenter_category_post AS cp
WHERE cp.post_id = kp.id WHERE cp.post_id = kp.id
AND cp.category_id = $categoryId AND cp.category_id = $categoryId
@@ -1321,20 +1461,39 @@ function get_post_list()
$categoryCondition = ""; $categoryCondition = "";
} }
// Betriebsrat darf nur Posts sehen, die in einer freigegebenen Kategorie liegen.
// Die erlaubten Kategorie-IDs werden von post_listform.php berechnet und übergeben,
// da get_permission_state() im Ajax-Kontext nicht verfügbar ist.
$brCategoryCondition = "";
$rawBrIds = isset($_POST['br_category_ids']) && $_POST['br_category_ids'] !== 'null'
? json_decode($_POST['br_category_ids'], true)
: null;
if (is_array($rawBrIds)) {
if (count($rawBrIds) === 0) {
$brCategoryCondition = "AND 1=0";
} else {
$brCatIdList = implode(',', array_map('intval', $rawBrIds));
$brCategoryCondition = "AND EXISTS (
SELECT 1 FROM knowledgecenter_category_post AS cp_br
WHERE cp_br.post_id = kp.id AND cp_br.category_id IN ($brCatIdList)
)";
}
}
// Beiträge abfragen (nur PostID und Titel der aktuellen Version) // Beiträge abfragen (nur PostID und Titel der aktuellen Version)
$sqlPosts = " $sqlPosts = "
SELECT SELECT
kp.id AS post_id, kp.id AS post_id,
pv.title AS current_title pv.title AS current_title
FROM knowledgecenter_posts AS kp FROM knowledgecenter_posts AS kp
LEFT JOIN knowledgecenter_post_versions AS pv LEFT JOIN knowledgecenter_post_versions AS pv
ON kp.id = pv.post_id ON kp.id = pv.post_id
AND pv.language_id = $languageId AND pv.language_id = $languageId
/* … Bedingungen für gültige Version und höchste version_number … */ /* … Bedingungen für gültige Version und höchste version_number … */
WHERE 1=1 WHERE 1=1
$searchCondition $searchCondition
$categoryCondition $categoryCondition
$brCategoryCondition
GROUP BY kp.id GROUP BY kp.id
ORDER BY kp.modified_at DESC ORDER BY kp.modified_at DESC
"; ";
@@ -1580,6 +1739,10 @@ function save_post_files()
echo json_encode(['error' => 'Ungültige Post-ID']); echo json_encode(['error' => 'Ungültige Post-ID']);
exit; exit;
} }
if (!ajax_can_edit_post($postId)) {
echo json_encode(['error' => 'Keine Berechtigung']);
exit;
}
if (!empty($filesStr)) { if (!empty($filesStr)) {
// Zerlege den String in einzelne Pfade (getrennt durch Komma und optional Leerzeichen) // Zerlege den String in einzelne Pfade (getrennt durch Komma und optional Leerzeichen)
@@ -1647,7 +1810,7 @@ function delete_post_file()
exit; exit;
} }
$selectQuery = "SELECT path FROM knowledgecenter_post_files WHERE id = $fileId LIMIT 1"; $selectQuery = "SELECT path, post_id FROM knowledgecenter_post_files WHERE id = $fileId LIMIT 1";
$selectResult = mysqli_query($GLOBALS['mysql_con'], $selectQuery); $selectResult = mysqli_query($GLOBALS['mysql_con'], $selectQuery);
if (!$selectResult || mysqli_num_rows($selectResult) === 0) { if (!$selectResult || mysqli_num_rows($selectResult) === 0) {
echo json_encode(['error' => 'Datei-Eintrag nicht gefunden']); echo json_encode(['error' => 'Datei-Eintrag nicht gefunden']);
@@ -1657,6 +1820,11 @@ function delete_post_file()
$row = mysqli_fetch_assoc($selectResult); $row = mysqli_fetch_assoc($selectResult);
$storedPath = trim((string) ($row['path'] ?? '')); $storedPath = trim((string) ($row['path'] ?? ''));
if (!ajax_can_edit_post((int)($row['post_id'] ?? 0))) {
echo json_encode(['error' => 'Keine Berechtigung']);
exit;
}
$userdataRoot = realpath($_SERVER['DOCUMENT_ROOT'] . '/userdata'); $userdataRoot = realpath($_SERVER['DOCUMENT_ROOT'] . '/userdata');
if ($userdataRoot === false) { if ($userdataRoot === false) {
echo json_encode(['error' => 'userdata-Verzeichnis nicht gefunden']); echo json_encode(['error' => 'userdata-Verzeichnis nicht gefunden']);
@@ -1811,7 +1979,7 @@ function search_posts()
if ($currentUserId) { if ($currentUserId) {
$query = "SELECT main_mandant_id, main_department_id, main_role_id, main_einricht_id, main_bereich_id $query = "SELECT main_mandant_id, main_department_id, main_role_id, main_einricht_id, main_bereich_id
FROM main_contact_department FROM main_contact_department
WHERE main_contact_id = $currentUserId AND active = 1"; WHERE main_contact_id = $currentUserId";
$result = mysqli_query($GLOBALS['mysql_con'], $query); $result = mysqli_query($GLOBALS['mysql_con'], $query);
while ($row = mysqli_fetch_assoc($result)) { while ($row = mysqli_fetch_assoc($result)) {
// Filtere 0 aus 0 gilt als "keine Zuordnung" // Filtere 0 aus 0 gilt als "keine Zuordnung"

View File

@@ -58,8 +58,10 @@ if (!$q || !($contactRow = mysqli_fetch_assoc($q))) {
$contactId = (int) $contactRow['id']; $contactId = (int) $contactRow['id'];
$masterMandantId = (int) $contactRow['master_mandant_id']; $masterMandantId = (int) $contactRow['master_mandant_id'];
if (get_permission_state('r-wiki', $masterMandantId, $contactId) != 1) { $hasRwiki = get_permission_state('r-wiki', $masterMandantId, $contactId) == 1;
respond_error('Keine Berechtigung (r-wiki).', 403); $hasRwikiBr = get_permission_state('r-wiki-br', $masterMandantId, $contactId) == 1;
if (!$hasRwiki && !$hasRwikiBr) {
respond_error('Keine Berechtigung.', 403);
} }
$postId = isset($_POST['post_id']) ? (int) $_POST['post_id'] : 0; $postId = isset($_POST['post_id']) ? (int) $_POST['post_id'] : 0;
@@ -72,6 +74,40 @@ if (!$verifyQ || mysqli_num_rows($verifyQ) === 0) {
respond_error('Beitrag existiert nicht.', 404); respond_error('Beitrag existiert nicht.', 404);
} }
// Betriebsrat darf Bilder nur für Posts in freigegebenen Kategorien hochladen (inkl. Vererbung)
if (!$hasRwiki && $hasRwikiBr) {
$brAllowed = false;
$brRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT category_id FROM knowledgecenter_category_post WHERE post_id = $postId");
while ($brRow = mysqli_fetch_assoc($brRes)) {
$catId = (int)$brRow['category_id'];
// Baum nach oben absuchen
$visited = [];
$queue = [$catId];
while (!empty($queue)) {
$current = array_shift($queue);
if (isset($visited[$current])) continue;
$visited[$current] = true;
$chkRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT betriebsrat_editable FROM knowledgecenter_categories_update WHERE id = $current LIMIT 1");
$chkRow = mysqli_fetch_assoc($chkRes);
if ($chkRow && (int)$chkRow['betriebsrat_editable'] === 1) {
$brAllowed = true;
break 2;
}
$pRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT parent_category_id FROM knowledgecenter_category_links WHERE child_category_id = $current");
while ($pRow = mysqli_fetch_assoc($pRes)) {
$pid = (int)$pRow['parent_category_id'];
if (!isset($visited[$pid])) $queue[] = $pid;
}
}
}
if (!$brAllowed) {
respond_error('Keine Berechtigung für diesen Beitrag.', 403);
}
}
if (!isset($_FILES['file']) || !is_array($_FILES['file'])) { if (!isset($_FILES['file']) || !is_array($_FILES['file'])) {
respond_error('Keine Datei empfangen.'); respond_error('Keine Datei empfangen.');
} }

View File

@@ -36,10 +36,17 @@
} }
.card_title { .card_title {
padding: 1rem; padding: 1rem 1rem 0.25rem;
font-weight: bold; font-weight: bold;
} }
.card_description {
padding: 0 1rem 0.75rem;
font-size: 0.82rem;
color: var(--text-secondary, #666);
line-height: 1.4;
}
.post-aside-section .collection_content.accept_certificate { .post-aside-section .collection_content.accept_certificate {
float: unset; float: unset;
} }
@@ -96,7 +103,10 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
flex-wrap: wrap; }
.categories_grid.list_view .card_description {
display: none;
} }
.titl .version_selected { .titl .version_selected {
@@ -164,6 +174,14 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
line-height: 1;
margin-bottom: 1em;
}
div#changeCategoryGridView svg {
width: 1em;
height: 1em;
display: block;
} }
.categories_grid.list_view { .categories_grid.list_view {
@@ -325,6 +343,9 @@
.titlebar .searchbar { .titlebar .searchbar {
flex: 1; flex: 1;
max-width: 500px; max-width: 500px;
display: flex;
align-items: center;
gap: 8px;
} }
.titlebar .searchbar input { .titlebar .searchbar input {
@@ -335,6 +356,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
box-sizing: border-box;
} }
div#filesDisplayContainer object { div#filesDisplayContainer object {
@@ -728,6 +750,12 @@
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.post-summary {
font-size: 0.9rem;
color: var(--text-secondary, #555);
line-height: 1.5;
}
.post-meta-item { .post-meta-item {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -930,8 +958,17 @@
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
} }
form#categories_posts_listform .promoted {
list-style: none;
display: flex;
align-items: center;
}
form#categories_posts_listform .promoted a { form#categories_posts_listform .promoted a {
padding: 7px 10px; padding: 7px 10px;
line-height: 1;
display: flex;
align-items: center;
background: var(--bg-light); background: var(--bg-light);
border-radius: var(--br); border-radius: var(--br);
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -1253,10 +1290,24 @@
color: white; color: white;
} }
.titlebar-actions {
display: flex;
align-items: center;
gap: 10px;
}
a[role="button"].green { a[role="button"].green {
background: #14b814; background: var(--clr-mandant);
border: 1px solid #14b814; border: 1px solid var(--clr-mandant);
padding: 7px 10px; color: #fff;
padding: 7px 14px;
border-radius: 4px;
font-weight: 600;
white-space: nowrap;
display: flex;
align-items: center;
line-height: 1;
margin-bottom: 1em;
} }
.post-action-btn.red { .post-action-btn.red {

View File

@@ -829,7 +829,6 @@ if (isset($_GET[$c_form]) && !empty($_POST)) {
// Helper-Funktion zur Berechtigungsprüfung // Helper-Funktion zur Berechtigungsprüfung
function require_permission($permissionCode) function require_permission($permissionCode)
{ {
// Hier nehmen wir an, dass $GLOBALS["main_contact"] korrekt gesetzt ist
if ( if (
get_permission_state( get_permission_state(
$permissionCode, $permissionCode,
@@ -842,6 +841,95 @@ function require_permission($permissionCode)
} }
} }
// Prüft, ob der User voller Wiki-Redakteur ist (r-wiki)
function is_wiki_redakteur(): bool
{
return get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1;
}
// Prüft, ob der User Betriebsrat-Wiki-Recht hat (r-wiki-br)
function is_wiki_betriebsrat(): bool
{
return get_permission_state('r-wiki-br', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1;
}
// Prüft, ob der User irgendeins der Wiki-Bearbeitungsrechte hat
function has_wiki_edit_permission(): bool
{
return is_wiki_redakteur() || is_wiki_betriebsrat();
}
function require_wiki_edit_permission(): void
{
if (!has_wiki_edit_permission()) {
echo "Sie haben nicht die notwendigen Berechtigungen, um diese Seite zu sehen.";
exit;
}
}
// Gibt alle Kategorie-IDs zurück, auf die Betriebsrat Zugriff hat:
// direkt betriebsrat_editable=1 sowie alle ihre Nachfahren im Baum (BFS nach unten).
function get_all_betriebsrat_accessible_category_ids(): array
{
$seeds = [];
$res = mysqli_query($GLOBALS['mysql_con'],
"SELECT id FROM knowledgecenter_categories_update WHERE betriebsrat_editable = 1");
while ($row = mysqli_fetch_assoc($res)) {
$seeds[] = (int)$row['id'];
}
if (empty($seeds)) {
return [];
}
$visited = [];
$queue = $seeds;
while (!empty($queue)) {
$current = array_shift($queue);
if (isset($visited[$current])) {
continue;
}
$visited[$current] = true;
$cRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT child_category_id FROM knowledgecenter_category_links WHERE parent_category_id = $current");
while ($cRow = mysqli_fetch_assoc($cRes)) {
$cid = (int)$cRow['child_category_id'];
if (!isset($visited[$cid])) {
$queue[] = $cid;
}
}
}
return array_keys($visited);
}
// Prüft ob eine Kategorie (oder irgendein Vorfahre über knowledgecenter_category_links)
// betriebsrat_editable=1 hat. Läuft den Baum nach oben durch.
function is_category_betriebsrat_editable(int $categoryId): bool
{
$visited = [];
$queue = [$categoryId];
while (!empty($queue)) {
$current = array_shift($queue);
if (isset($visited[$current])) {
continue;
}
$visited[$current] = true;
$res = mysqli_query($GLOBALS['mysql_con'],
"SELECT betriebsrat_editable FROM knowledgecenter_categories_update WHERE id = $current LIMIT 1");
$row = mysqli_fetch_assoc($res);
if ($row && (int)$row['betriebsrat_editable'] === 1) {
return true;
}
$pRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT parent_category_id FROM knowledgecenter_category_links WHERE child_category_id = $current");
while ($pRow = mysqli_fetch_assoc($pRes)) {
$pid = (int)$pRow['parent_category_id'];
if (!isset($visited[$pid])) {
$queue[] = $pid;
}
}
}
return false;
}
// Switch-Case für die verschiedenen Aktionen // Switch-Case für die verschiedenen Aktionen
$action = $_GET["action"] ?? "List"; $action = $_GET["action"] ?? "List";
switch ($action) { switch ($action) {
@@ -850,6 +938,7 @@ switch ($action) {
require_once(__DIR__ . '/../Installation/Installation.php'); require_once(__DIR__ . '/../Installation/Installation.php');
break; break;
case "Categories": case "Categories":
// Kategorieverwaltung nur für vollständige Redakteure
if (isset($_GET['detail'])) { if (isset($_GET['detail'])) {
require_permission('r-wiki'); require_permission('r-wiki');
require_once(__DIR__ . '/../Views/categories_cardform.php'); require_once(__DIR__ . '/../Views/categories_cardform.php');
@@ -859,7 +948,8 @@ switch ($action) {
} }
break; break;
case "PostList": case "PostList":
require_permission('r-wiki'); // Betriebsrat darf die Post-Liste sehen (gefiltert auf seine Kategorien)
require_wiki_edit_permission();
require_once(__DIR__ . '/../Views/post_listform.php'); require_once(__DIR__ . '/../Views/post_listform.php');
break; break;
case "Post": case "Post":
@@ -870,7 +960,8 @@ switch ($action) {
require_once(__DIR__ . '/../Import/Import.php'); require_once(__DIR__ . '/../Import/Import.php');
break; break;
case "NewPost": case "NewPost":
require_permission('r-wiki'); // Betriebsrat darf neue Posts erstellen (nur in freigegebenen Kategorien)
require_wiki_edit_permission();
$_GET['new'] = 1; $_GET['new'] = 1;
require_once(__DIR__ . '/../Views/post_cardform.php'); require_once(__DIR__ . '/../Views/post_cardform.php');
break; break;

View File

@@ -13,7 +13,7 @@ $userFachbereiche = [];
if ($currentUserId) { if ($currentUserId) {
$query = "SELECT main_mandant_id, main_department_id, main_role_id, main_einricht_id, main_bereich_id $query = "SELECT main_mandant_id, main_department_id, main_role_id, main_einricht_id, main_bereich_id
FROM main_contact_department FROM main_contact_department
WHERE main_contact_id = $currentUserId AND active = 1"; WHERE main_contact_id = $currentUserId";
$result = mysqli_query($GLOBALS['mysql_con'], $query); $result = mysqli_query($GLOBALS['mysql_con'], $query);
while ($row = mysqli_fetch_assoc($result)) { while ($row = mysqli_fetch_assoc($result)) {
// Filtere 0 aus 0 gilt als "keine Zuordnung" // Filtere 0 aus 0 gilt als "keine Zuordnung"

View File

@@ -1,13 +1,23 @@
<?php <?php
// Prüft, ob ein Session-Cookie mit bekanntem Namen existiert (sidintranet, mysyde oder PHPSESSID) // Find active session cookie: try any sid* cookie first, then fallbacks.
// und übernimmt diesen Namen für die aktuelle Session. $_kc_found = false;
foreach (['sidintranet', 'mysyde', 'PHPSESSID'] as $n) { foreach (array_keys($_COOKIE) as $_kc_n) {
if (isset($_COOKIE[$n])) { if (strncmp($_kc_n, 'sid', 3) === 0) {
if (session_name() !== $n) session_name($n); if (session_name() !== $_kc_n) session_name($_kc_n);
$_kc_found = true;
break; break;
} }
} }
if (!$_kc_found) {
foreach (['mysyde', 'PHPSESSID'] as $_kc_n) {
if (isset($_COOKIE[$_kc_n])) {
if (session_name() !== $_kc_n) session_name($_kc_n);
break;
}
}
}
unset($_kc_found, $_kc_n);
// Bestimmt, ob die Verbindung sicher (HTTPS) ist, um Cookie-Parameter korrekt zu setzen. // Bestimmt, ob die Verbindung sicher (HTTPS) ist, um Cookie-Parameter korrekt zu setzen.
$isSecure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['SERVER_PORT'] ?? 80) == 443); $isSecure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['SERVER_PORT'] ?? 80) == 443);

View File

@@ -1,15 +1,25 @@
<script> <script>
// Redakteur (r-wiki) darf frei navigieren; Betriebsrat ist auf den Post-Ordner beschränkt
var kcUserLockedToPostFolder = <?= (!is_wiki_redakteur() && is_wiki_betriebsrat()) ? 'true' : 'false' ?>;
function buildFileGalleryUrl(target, mode, ajaxFunction, nocache) { function buildFileGalleryUrl(target, mode, ajaxFunction, nocache) {
var base = "/module/knowledgecenter_update/Plugins/filesgallery.php?"; var base = "/module/knowledgecenter_update/Plugins/filesgallery.php?";
var startPath = (typeof window.kcPostGalleryPath === "string") var postPath = (typeof window.kcPostGalleryPath === "string")
? window.kcPostGalleryPath.replace(/^\/+|\/+$/g, "") ? window.kcPostGalleryPath.replace(/^\/+|\/+$/g, "")
: ""; : "";
// Redakteur: kein Lock (leerer kc_lock_path löscht den Session-Lock)
// Betriebsrat: immer auf Post-Ordner beschränken
var lockPath = kcUserLockedToPostFolder ? postPath : "";
var params = "input_id=" + encodeURIComponent(target) + var params = "input_id=" + encodeURIComponent(target) +
"&mode=" + encodeURIComponent(mode) + "&mode=" + encodeURIComponent(mode) +
"&saveajax=" + encodeURIComponent(ajaxFunction || "") + "&saveajax=" + encodeURIComponent(ajaxFunction || "") +
"&kc_lock_path=" + encodeURIComponent(startPath) + "&kc_lock_path=" + encodeURIComponent(lockPath) +
"&nocache=" + nocache; "&nocache=" + nocache;
// Startverzeichnis: bei Betriebsrat immer Post-Ordner, bei Redakteur auch
var startPath = postPath;
if (startPath.length > 0) { if (startPath.length > 0) {
return base + encodeURIComponent(startPath) + "&" + params; return base + encodeURIComponent(startPath) + "&" + params;
} }

View File

@@ -0,0 +1,473 @@
<?php
/**
* Migration: Alte Wiki-Artikel (main_collection) → Neue Wiki-Struktur (knowledgecenter_posts)
*
* CLI-only. Idempotent (über legacy_collection_id). Pro Artikel transaktional.
*
* Aufruf:
* php migrate_old_wiki.php --dry-run --collection-id=1491
* php migrate_old_wiki.php --apply --collection-id=1491 --api-key=KEY
* php migrate_old_wiki.php --apply --limit=5 --api-key=KEY
* php migrate_old_wiki.php --apply (alle Artikel)
*
* Optionen:
* --dry-run Standard. Nur analysieren, nichts schreiben.
* --apply Wirklich migrieren (DB + Dateien).
* --collection-id=N Nur diesen Artikel migrieren.
* --limit=N Maximal N Artikel migrieren (für Tests).
* --api-key=KEY Gemini API Key (oder via GEMINI_API_KEY env).
* --skip-summaries Keine KI-Zusammenfassung generieren.
* --verbose Mehr Details ausgeben.
* --help Diese Hilfe.
*/
if (PHP_SAPI !== 'cli') {
fwrite(STDERR, "Dieses Skript ist CLI-only.\n");
exit(2);
}
$currDir = __DIR__;
$rootDir = dirname(dirname(dirname($currDir)));
$mysydeDir = $rootDir . '/mysyde';
require_once($rootDir . '/vendor/autoload.php');
$envDir = $rootDir . '/config';
$envFilePath = $envDir . '/.env';
if (is_dir($envDir) && file_exists($envFilePath) && is_readable($envFilePath)) {
$dotenv = new \Dotenv\Dotenv($envDir);
$dotenv->load();
}
require_once($mysydeDir . '/common/common_functions.inc.php');
$connEnv = (local_environment()) ? $mysydeDir . '/dc.config.php' : $mysydeDir . '/dc-server.config.php';
require_once($connEnv);
db_connect();
// ------- Args -------
$opts = [
'dry-run' => true,
'apply' => false,
'collection-id' => null,
'limit' => null,
'api-key' => getenv('GEMINI_API_KEY') ?: null,
'skip-summaries' => false,
'summaries-only' => false,
'verbose' => false,
];
foreach (array_slice($argv, 1) as $arg) {
if ($arg === '--help' || $arg === '-h') {
echo "Usage: php migrate_old_wiki.php [--dry-run|--apply] [--collection-id=N] [--limit=N] [--api-key=KEY] [--skip-summaries] [--verbose]\n";
exit(0);
}
if ($arg === '--apply') { $opts['apply'] = true; $opts['dry-run'] = false; continue; }
if ($arg === '--dry-run') { $opts['dry-run'] = true; $opts['apply'] = false; continue; }
if ($arg === '--skip-summaries') { $opts['skip-summaries'] = true; continue; }
if ($arg === '--summaries-only') { $opts['summaries-only'] = true; continue; }
if ($arg === '--verbose') { $opts['verbose'] = true; continue; }
if (preg_match('/^--collection-id=(\d+)$/', $arg, $m)) { $opts['collection-id'] = (int)$m[1]; continue; }
if (preg_match('/^--limit=(\d+)$/', $arg, $m)) { $opts['limit'] = (int)$m[1]; continue; }
if (preg_match('/^--api-key=(.+)$/', $arg, $m)) { $opts['api-key'] = $m[1]; continue; }
fwrite(STDERR, "Unbekannte Option: $arg\n");
exit(2);
}
$dryRun = !$opts['apply'];
$verbose = $opts['verbose'];
$apiKey = $opts['api-key'];
$skipSummary = $opts['skip-summaries'];
$userdataDir = $rootDir . '/userdata';
// ------- Log-Datei -------
$logDir = $userdataDir . '/knowledgecenter_update/migrations';
if (!is_dir($logDir)) @mkdir($logDir, 0775, true);
$logFile = $logDir . '/wiki_migration_' . gmdate('Ymd_His') . '.log';
$logFp = fopen($logFile, 'w');
if (!$logFp) { fwrite(STDERR, "Log-Datei nicht anlegbar: $logFile\n"); exit(3); }
fwrite(STDOUT, "Log: $logFile\n");
// ------- Logging -------
function logmsg(string $msg, string $level = 'INFO'): void {
global $verbose, $logFp;
$line = '[' . gmdate('Y-m-d H:i:s') . " UTC] [$level] $msg\n";
fwrite($logFp, $line);
if ($verbose || in_array($level, ['ERROR', 'FAIL', 'OK', 'WARN', 'HEAD', 'SKIP', 'PROMPT'], true)) {
fwrite(STDOUT, $line);
}
}
// ------- Phase 0: Schema-Vorbereitung -------
logmsg("Prüfe Schema ...", 'HEAD');
mysqli_query($GLOBALS['mysql_con'],
"ALTER TABLE knowledgecenter_posts ADD COLUMN IF NOT EXISTS legacy_collection_id INT NULL");
mysqli_query($GLOBALS['mysql_con'],
"CREATE UNIQUE INDEX IF NOT EXISTS idx_legacy_collection ON knowledgecenter_posts(legacy_collection_id)");
// ------- Hilfsfunktionen -------
/**
* Returns ['teaser' => string, 'content' => string]
* setup_content_id=55 (kc_teaser, data-field) → teaser
* setup_content_id=58 (kc_article, textcontent_header) → content
*/
function assemble_content(int $cid): array {
$sql = "SELECT mcl.main_collection_setup_content_id, mcl.main_sitepart_id,
mcl.main_sitepart_header_id, mcl.data
FROM main_collection_link mcl
WHERE mcl.main_collection_id = $cid
ORDER BY mcl.id";
$res = mysqli_query($GLOBALS['mysql_con'], $sql);
$teaser = '';
$content = '';
while ($row = mysqli_fetch_assoc($res)) {
$scid = (int)$row['main_collection_setup_content_id'];
if ($scid === 55) {
// Teaser: plain text im data-Feld, keine HTML-Tags
$text = trim($row['data'] ?? '');
if ($text !== '') {
$teaser .= ($teaser !== '' ? ' ' : '') . $text;
}
} elseif ($row['main_sitepart_id'] == 1 && $row['main_sitepart_header_id'] > 0) {
// Artikeltext: HTML aus textcontent_header (setup_content_id=58 oder ähnlich)
$hid = (int)$row['main_sitepart_header_id'];
$hres = mysqli_query($GLOBALS['mysql_con'],
"SELECT content FROM textcontent_header WHERE id = $hid LIMIT 1");
if ($hrow = mysqli_fetch_assoc($hres)) {
$content .= $hrow['content'];
}
}
}
return ['teaser' => $teaser, 'content' => $content];
}
function get_article_files(int $cid): array {
// sitepart_id 5, 6, 10 sind filegallery-Typen; sitepart_id=1 ist textcontent_header (nicht joinen!)
$sql = "SELECT DISTINCT fl.filename, fl.extension, fl.description
FROM main_collection_link mcl
JOIN filegallery_line fl ON fl.header_id = mcl.main_sitepart_header_id
WHERE mcl.main_collection_id = $cid
AND mcl.main_sitepart_id IN (5, 6, 10)
AND mcl.main_sitepart_header_id > 0
AND fl.filename != ''
ORDER BY fl.sorting";
$res = mysqli_query($GLOBALS['mysql_con'], $sql);
$files = [];
while ($row = mysqli_fetch_assoc($res)) {
$files[] = $row;
}
return $files;
}
function extract_pdf_text(string $filePath): string {
if (!file_exists($filePath) || filesize($filePath) === 0) return '';
// pdftotext ist robuster bei gesicherten PDFs (nur Kopier-Schutz, kein Passwort)
$pdftotext = trim(shell_exec('which pdftotext 2>/dev/null') ?? '');
if ($pdftotext !== '') {
$escaped = escapeshellarg($filePath);
$out = shell_exec("$pdftotext -nopgbrk $escaped - 2>/dev/null");
if ($out !== null && strlen(trim($out)) > 10) {
return preg_replace('/\s+/', ' ', trim($out));
}
}
// Fallback: pdfparser
try {
$parser = new \Smalot\PdfParser\Parser();
$pdf = $parser->parseFile($filePath);
$text = trim($pdf->getText());
return preg_replace('/\s+/', ' ', $text);
} catch (\Throwable $e) {
return '';
}
}
function generate_summary(string $htmlContent, array $files, string $userdataDir, string $apiKey, string $title, string $teaser = ''): ?string {
$parts = [];
// Teaser bevorzugen: bereits plain text, sehr kurz → spart Input-Tokens
if (strlen($teaser) > 30) {
$parts[] = $teaser;
}
// Fließtext: HTML entfernen, auf 1500 Zeichen cappen
$plainText = html_entity_decode(strip_tags($htmlContent), ENT_HTML5 | ENT_QUOTES, 'UTF-8');
$plainText = trim(preg_replace('/\s+/', ' ', $plainText));
if (strlen($plainText) > 30) {
$parts[] = mb_substr($plainText, 0, 1500);
}
// PDFs nur wenn sonst kein Inhalt vorhanden, auf 800 Zeichen begrenzen
if (empty($parts)) {
foreach ($files as $file) {
if (strtolower($file['extension']) !== 'pdf') continue;
$pdfText = extract_pdf_text($userdataDir . '/' . $file['filename']);
if (strlen($pdfText) > 50) {
$label = $file['description'] ?: basename($file['filename']);
$parts[] = 'Anhang "' . $label . '": ' . mb_substr($pdfText, 0, 800);
break; // ein PDF reicht
}
}
}
if (empty($parts)) {
return null;
}
$prompt = "Fasse in 2-3 deutschen Sätzen zusammen. Nur Zusammenfassung.\nTitel: $title\n\n"
. implode("\n\n", $parts);
logmsg(" PROMPT (" . strlen($prompt) . " Zeichen)", 'PROMPT');
$payload = json_encode([
'contents' => [['parts' => [['text' => $prompt]]]],
'generationConfig' => [
'maxOutputTokens' => 150,
'temperature' => 0.2,
'thinkingConfig' => ['thinkingBudget' => 0],
],
]);
$url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=' . urlencode($apiKey);
$resp = '';
$httpCode = 0;
$attempts = [[0, null], [5, 503], [15, 503], [30, 429], [60, 429]];
foreach ($attempts as [$wait, $label]) {
if ($wait > 0) {
logmsg(" Gemini $httpCode warte {$wait}s (retry) ...", 'WARN');
sleep($wait);
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => $payload,
]);
$resp = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 503 && $httpCode !== 429) break;
}
if ($httpCode !== 200) {
logmsg("Gemini API Fehler HTTP $httpCode: " . substr($resp, 0, 200), 'WARN');
return null;
}
$data = json_decode($resp, true);
$summary = trim($data['candidates'][0]['content']['parts'][0]['text'] ?? '');
if (strlen($summary) < 30) {
logmsg(" Gemini Roh-Antwort: " . substr($resp, 0, 500), 'WARN');
}
return $summary;
}
// ------- Permissions aus main_collection auf Post übertragen -------
function copy_permissions_to_post(int $postId, int $collectionId): void {
$maps = [
['src' => 'main_collection_mandant_link', 'src_col' => 'main_mandant_id', 'dst' => 'knowledgecenter_post_mandant', 'dst_col' => 'mandant_id'],
['src' => 'main_collection_department_link', 'src_col' => 'main_department_id', 'dst' => 'knowledgecenter_post_department', 'dst_col' => 'department_id'],
['src' => 'main_collection_role_link', 'src_col' => 'main_role_id', 'dst' => 'knowledgecenter_post_role', 'dst_col' => 'role_id'],
['src' => 'main_collection_einricht_link', 'src_col' => 'main_einricht_id', 'dst' => 'knowledgecenter_post_einricht', 'dst_col' => 'einricht_id'],
['src' => 'main_collection_fachbereich_link', 'src_col' => 'main_fachbereich_id', 'dst' => 'knowledgecenter_post_fachbereich', 'dst_col' => 'fachbereich_id'],
];
foreach ($maps as $m) {
$sql = "INSERT IGNORE INTO {$m['dst']} (post_id, {$m['dst_col']})
SELECT $postId, {$m['src_col']} FROM {$m['src']} WHERE main_collection_id = $collectionId";
mysqli_query($GLOBALS['mysql_con'], $sql);
}
}
// ------- Artikel laden -------
$whereClause = "mc.main_collection_setup_id = 3 AND mc.is_archived = 0 AND mc.active = 1 AND mc.description != ''";
if ($opts['collection-id'] !== null) {
$whereClause .= " AND mc.id = " . (int)$opts['collection-id'];
}
// ------- Summaries-only Modus: fehlende Summaries für bereits migrierte Posts nachfüllen -------
if ($opts['summaries-only']) {
if (!$apiKey) { fwrite(STDERR, "--summaries-only benötigt --api-key\n"); exit(2); }
$sumSql = "SELECT p.id as post_id, p.legacy_collection_id, pv.id as version_id, pv.title,
pv.teaser, pv.content
FROM knowledgecenter_posts p
JOIN knowledgecenter_post_versions pv ON pv.post_id = p.id AND pv.version_number = 1
WHERE (pv.summary IS NULL OR pv.summary = '')
AND p.legacy_collection_id IS NOT NULL";
if ($opts['collection-id'] !== null) {
$sumSql .= " AND p.legacy_collection_id = " . (int)$opts['collection-id'];
}
if ($opts['limit'] !== null) {
$sumSql .= " LIMIT " . (int)$opts['limit'];
}
$sumResult = mysqli_query($GLOBALS['mysql_con'], $sumSql);
$sumTotal = mysqli_num_rows($sumResult);
logmsg("Summaries-only: $sumTotal Posts ohne Summary", 'HEAD');
$sumStats = ['ok' => 0, 'fail' => 0];
while ($sumRow = mysqli_fetch_assoc($sumResult)) {
$postId = (int)$sumRow['post_id'];
$collId = (int)$sumRow['legacy_collection_id'];
$versionId = (int)$sumRow['version_id'];
$title = $sumRow['title'];
$files = get_article_files($collId);
$teaser = $sumRow['teaser'] ?? '';
$summary = generate_summary($sumRow['content'], $files, $userdataDir, $apiKey, $title, $teaser) ?? '';
if ($summary === '') {
logmsg(" [FAIL] Post $postId \"$title\": leere Summary", 'WARN');
$sumStats['fail']++;
} else {
$sumEsc = mysqli_real_escape_string($GLOBALS['mysql_con'], $summary);
mysqli_query($GLOBALS['mysql_con'],
"UPDATE knowledgecenter_post_versions SET summary = '$sumEsc' WHERE id = $versionId");
logmsg("[OK ] Post $postId \"$title\": " . mb_substr($summary, 0, 80), 'OK');
$sumStats['ok']++;
}
sleep(4); // 15 RPM Free-Tier: mind. 4 Sek. zwischen Requests
}
logmsg(sprintf("Summaries fertig. ok=%d fail=%d", $sumStats['ok'], $sumStats['fail']), 'HEAD');
logmsg("Log-Datei: $logFile", 'HEAD');
fclose($logFp);
exit(0);
}
$sql = "SELECT mc.id, mc.description, mc.creation_date, mc.modified_date,
mc.validity_from, mc.validity_to
FROM main_collection mc
WHERE $whereClause
ORDER BY mc.id";
if ($opts['limit'] !== null) {
$sql .= " LIMIT " . (int)$opts['limit'];
}
$result = mysqli_query($GLOBALS['mysql_con'], $sql);
$total = mysqli_num_rows($result);
logmsg("Modus=" . ($dryRun ? 'DRY-RUN' : 'APPLY') . " Artikel=$total Limit=" .
($opts['limit'] ?? 'alle') . " API=" . ($apiKey && !$skipSummary ? 'ja' : 'nein'), 'HEAD');
$stats = ['ok' => 0, 'skipped' => 0, 'fail' => 0];
while ($row = mysqli_fetch_assoc($result)) {
$cid = (int)$row['id'];
$title = $row['description'];
// Idempotenz-Check
$already = mysqli_query($GLOBALS['mysql_con'],
"SELECT id FROM knowledgecenter_posts WHERE legacy_collection_id = $cid LIMIT 1");
if ($already && mysqli_num_rows($already) > 0) {
logmsg("Artikel $cid \"$title\" bereits migriert, überspringe.", 'SKIP');
$stats['skipped']++;
continue;
}
$parts = assemble_content($cid);
$teaserHtml = $parts['teaser'];
$htmlContent = $parts['content'];
$files = get_article_files($cid);
$htmlLen = strlen($htmlContent);
$catRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT knowledgecenter_category_id FROM knowledgecenter_collection_link WHERE main_collection_id = $cid");
$catIds = [];
while ($cr = mysqli_fetch_assoc($catRes)) {
$catIds[] = (int)$cr['knowledgecenter_category_id'];
}
logmsg("Artikel $cid \"$title\" | Teaser=" . strlen($teaserHtml) . "B | HTML=" . $htmlLen . "B | Dateien=" . count($files) .
" | Kategorien=[" . implode(',', $catIds) . "]");
if ($dryRun) {
$stats['ok']++;
continue;
}
// Summary generieren (vor der Transaktion, API-Call außerhalb)
$summary = '';
if (!$skipSummary && $apiKey) {
$fullContent = $teaserHtml . $htmlContent;
$generated = generate_summary($fullContent, $files, $userdataDir, $apiKey, $title);
$summary = $generated ?? '';
logmsg(" Summary: " . (strlen($summary) > 0 ? '"' . mb_substr($summary, 0, 100) . '"' : '(leer)'));
sleep(4); // 15 RPM Free-Tier: mind. 4 Sek. zwischen Requests
}
// HTML-Entities dekodieren für saubere UTF-8 Speicherung
$cleanHtml = html_entity_decode($htmlContent, ENT_HTML5 | ENT_QUOTES, 'UTF-8');
mysqli_query($GLOBALS['mysql_con'], 'START TRANSACTION');
try {
$titleEsc = mysqli_real_escape_string($GLOBALS['mysql_con'], $title);
$summaryEsc = mysqli_real_escape_string($GLOBALS['mysql_con'], $summary);
$teaserEsc = mysqli_real_escape_string($GLOBALS['mysql_con'], $teaserHtml);
$contentEsc = mysqli_real_escape_string($GLOBALS['mysql_con'], $cleanHtml);
$createdAt = date('Y-m-d H:i:s', (int)$row['creation_date']);
$modifiedAt = date('Y-m-d H:i:s', (int)$row['modified_date']);
$validFrom = $row['validity_from'] ? "'" . $row['validity_from'] . "'" : 'NULL';
$validUntil = $row['validity_to'] ? "'" . $row['validity_to'] . "'" : 'NULL';
// knowledgecenter_posts
$sqlPost = "INSERT INTO knowledgecenter_posts (legacy_collection_id, created_at, modified_at)
VALUES ($cid, '$createdAt', '$modifiedAt')";
if (!mysqli_query($GLOBALS['mysql_con'], $sqlPost)) {
throw new RuntimeException("Post-Insert: " . mysqli_error($GLOBALS['mysql_con']));
}
$postId = (int)mysqli_insert_id($GLOBALS['mysql_con']);
// knowledgecenter_post_versions
$sqlVer = "INSERT INTO knowledgecenter_post_versions
(post_id, version_number, language_id, title, teaser, content, summary, state, modified_at, valid_from, valid_until)
VALUES
($postId, 1, 1, '$titleEsc', '$teaserEsc', '$contentEsc', '$summaryEsc', 1, '$modifiedAt', $validFrom, $validUntil)";
if (!mysqli_query($GLOBALS['mysql_con'], $sqlVer)) {
throw new RuntimeException("Version-Insert: " . mysqli_error($GLOBALS['mysql_con']));
}
// Kategorie-Links
foreach ($catIds as $catId) {
mysqli_query($GLOBALS['mysql_con'],
"INSERT IGNORE INTO knowledgecenter_category_post (category_id, post_id) VALUES ($catId, $postId)");
}
// Permissions aus main_collection auf Post übertragen
copy_permissions_to_post($postId, $cid);
// Dateiverknüpfungen: Original-Pfad beibehalten, keine Kopie
foreach ($files as $file) {
$srcPath = $userdataDir . '/' . $file['filename'];
if (!file_exists($srcPath)) {
logmsg(" Quelldatei nicht gefunden: " . $file['filename'], 'WARN');
continue;
}
$relPath = '/userdata/' . $file['filename'];
$descEsc = mysqli_real_escape_string($GLOBALS['mysql_con'], $file['description'] ?: basename($file['filename']));
$pathEsc = mysqli_real_escape_string($GLOBALS['mysql_con'], $relPath);
mysqli_query($GLOBALS['mysql_con'],
"INSERT INTO knowledgecenter_post_files (post_id, description, path)
VALUES ($postId, '$descEsc', '$pathEsc')");
logmsg(" Datei verknüpft: " . $file['filename']);
}
mysqli_query($GLOBALS['mysql_con'], 'COMMIT');
logmsg("[OK ] Artikel $cid Post $postId \"$title\"", 'OK');
$stats['ok']++;
} catch (\Throwable $e) {
mysqli_query($GLOBALS['mysql_con'], 'ROLLBACK');
logmsg("[FAIL] Artikel $cid \"$title\": " . $e->getMessage(), 'FAIL');
$stats['fail']++;
}
}
logmsg(sprintf("Fertig. ok=%d skipped=%d fail=%d modus=%s",
$stats['ok'], $stats['skipped'], $stats['fail'],
$dryRun ? 'DRY-RUN' : 'APPLY'), 'HEAD');
logmsg("Log-Datei: $logFile", 'HEAD');
fclose($logFp);

View File

@@ -134,6 +134,13 @@ while ($row = mysqli_fetch_assoc($resultSavedEinrichts)) {
$savedEinrichts[] = $row['einricht_id']; $savedEinrichts[] = $row['einricht_id'];
} }
// Verknüpfungskarte für Einrichtungen (Gruppen → Mitglieder)
$einrichtLinks = [];
$linkRes = mysqli_query($GLOBALS['mysql_con'], "SELECT einricht_id, linked_einricht_id FROM organigramm_einricht_link");
while ($lr = mysqli_fetch_assoc($linkRes)) {
$einrichtLinks[strval($lr['einricht_id'])][] = strval($lr['linked_einricht_id']);
}
// Array mit Daten für Tagify (Einrichtungen) // Array mit Daten für Tagify (Einrichtungen)
$savedEinrichtsData = []; $savedEinrichtsData = [];
foreach ($einrichts as $einricht) { foreach ($einrichts as $einricht) {
@@ -232,62 +239,17 @@ while ($row = mysqli_fetch_assoc($resultParents)) {
$parentCount = count($parentCategories); $parentCount = count($parentCategories);
?> ?>
<style> <style>
.cf-layout{background:#fff;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.1);overflow:hidden;} .perm-row { margin-bottom: 16px; }
.cf-topbar{display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:2px solid #e5e7eb;} .perm-row label { display: block; font-weight: 500; font-size: 13px; color: #374151; margin-bottom: 6px; }
.cf-topbar-back{font-size:18px;text-decoration:none;color:#374151;padding:4px 8px;border-radius:4px;} .perm-row .tagify { width: 100%; }
.cf-topbar-back:hover{background:#f3f4f6;}
.cf-section-tabs{display:flex;gap:4px;flex:1;}
.cf-section-tab{padding:6px 18px;border:1px solid #d1d5db;border-radius:6px;background:#f9fafb;cursor:pointer;font-size:13px;color:#374151;font-weight:500;line-height:1.4;}
.cf-section-tab.active{background:#3B82F6;color:#fff;border-color:#3B82F6;}
.cf-topbar-actions{display:flex;gap:6px;align-items:center;margin-left:auto;}
.cf-tab-pane{display:none;padding:20px;}
.cf-tab-pane.active{display:block;}
.cf-general-layout{display:grid;grid-template-columns:1fr 260px;gap:24px;align-items:start;}
.cf-settings-col{border-left:1px solid #e5e7eb;padding-left:20px;display:flex;flex-direction:column;gap:12px;}
.cf-settings-col label{display:flex;justify-content:space-between;align-items:center;gap:8px;font-size:14px;color:#374151;cursor:pointer;}
.cf-settings-meta{font-size:12px;color:#6b7280;margin-top:8px;}
.cf-settings-meta ul{margin:4px 0;padding-left:16px;}
.cf-settings-meta a{color:#3B82F6;}
.cf-perm-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:20px;align-items:start;}
.cf-perm-sm{grid-column:span 2;}
.cf-perm-lg{grid-column:span 3;}
.cf-perm-block h4{font-size:13px;font-weight:600;margin:0 0 8px 0;color:#374151;}
</style> </style>
<div class="cf-layout"> <div class="cardform_containern">
<!-- Top bar: navigation, section tabs, save/delete -->
<div class="cf-topbar">
<a class="cf-topbar-back" href="?action=Categories&embedded=<?= $isEmbedded ?>">←</a>
<div class="cf-section-tabs">
<button class="cf-section-tab active" data-target="cf-tab-allgemein">Allgemein</button>
<?php if ($categoryId != 0): ?>
<button class="cf-section-tab" data-target="cf-tab-berechtigungen">Berechtigungen</button>
<button class="cf-section-tab" data-target="cf-tab-unterkategorien">Unterkategorien</button>
<?php endif; ?>
</div>
<div class="cf-topbar-actions">
<button id="message" style="opacity:0; pointer-events:none; min-width:1px;"></button>
<?php if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1): ?>
<button class="post-action-btn" id="saveAllTranslations" style="background:#14b814;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-check" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"/>
</svg>
</button>
<button class="post-action-btn red" id="deleteCategory">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-trash3-fill" viewBox="0 0 16 16">
<path d="M11 1.5v1h3.5a.5.5 0 0 1 0 1h-.538l-.853 10.66A2 2 0 0 1 11.115 16h-6.23a2 2 0 0 1-1.994-1.84L2.038 3.5H1.5a.5.5 0 0 1 0-1H5v-1A1.5 1.5 0 0 1 6.5 0h3A1.5 1.5 0 0 1 11 1.5m-5 0v1h4v-1a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5M4.5 5.029l.5 8.5a.5.5 0 1 0 .998-.06l-.5-8.5a.5.5 0 1 0-.998.06m6.53-.528a.5.5 0 0 0-.528.47l-.5 8.5a.5.5 0 0 0 .998.058l.5-8.5a.5.5 0 0 0-.47-.528M8 4.5a.5.5 0 0 0-.5.5v8.5a.5.5 0 0 0 1 0V5a.5.5 0 0 0-.5-.5"/>
</svg>
</button>
<?php endif; ?>
</div>
</div>
<!-- Tab: Allgemein --> <div class="cardform">
<div id="cf-tab-allgemein" class="cf-tab-pane active"> <div class="card-body">
<div class="cf-general-layout"> <div class="tabbar_with_button">
<div> <a href="?action=Categories&embedded=<?= $isEmbedded ?>">←</a>
<ul class="nav nav-tabs" id="languageTabs" role="tablist"> <ul class="nav nav-tabs" id="languageTabs" role="tablist">
<?php foreach ($languages as $index => $lang): ?> <?php foreach ($languages as $index => $lang): ?>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
@@ -298,189 +260,240 @@ $parentCount = count($parentCategories);
</li> </li>
<?php endforeach; ?> <?php endforeach; ?>
</ul> </ul>
<div class="tab-content-languages" id="languageTabsContent"> <div>
<?php foreach ($languages as $index => $lang): <button id="message" style="opacity:0;"></button>
$trans = $translations[$lang['code']];
?>
<div class="tab-pane fade <?= ($index === 0) ? 'show active' : '' ?>" id="lang-<?= $lang['code'] ?>"
role="tabpanel">
<form class="translationForm" data-lang="<?= $lang['id'] ?>" data-lang-code="<?= $lang['code'] ?>">
<div class="label title-<?php echo $lang['code']; ?>">
<label for="title-<?php echo $lang['code']; ?>">Titel
(<?php echo htmlspecialchars($lang['code']); ?>)</label>
</div>
<div class="input title-<?php echo $lang['code']; ?>">
<input required name="title-<?php echo $lang['code']; ?>"
id="title-<?php echo $lang['code']; ?>" class="text" type="text"
value="<?php echo htmlspecialchars($translations[$lang['code']]['title']); ?>">
</div>
<?php if ($categoryId != 0): ?>
<div class="single_image_group">
<img id="preview-<?= $lang['code'] ?>"
src="<?= $trans['image'] ? '/userdata/' . htmlspecialchars($trans['image']) : '/userdata/intranet/mandant/platzhalter_kategorie.png' ?>"
alt="Bildvorschau" style="max-width: 100px; min-height: 60px;
border-radius: 5px; <?= $trans['image'] ? '' : 'background-color:#f3f3f3;' ?>">
<input type="hidden" name="image-<?= $lang['code'] ?>" id="image-<?= $lang['code'] ?>"
value="<?= htmlspecialchars($trans['image']) ?>">
<div class="post-action-btn green openFileGallery" data-ajax-function="saveCategoryAjax"
data-target="image-<?= $lang['code'] ?>">+</div>
<div class="post-action-btn clearImage" data-target="image-<?= $lang['code'] ?>">✕</div>
</div>
<?php endif; ?>
</form>
</div>
<?php endforeach; ?>
</div> </div>
</div> </div>
<div class="tab-content-languages" id="languageTabsContent">
<?php foreach ($languages as $index => $lang):
$trans = $translations[$lang['code']];
?>
<div class="tab-pane fade <?= ($index === 0) ? 'show active' : '' ?>" id="lang-<?= $lang['code'] ?>"
role="tabpanel">
<form class="translationForm" data-lang="<?= $lang['id'] ?>" data-lang-code="<?= $lang['code'] ?>">
<div class="label title-<?php echo $lang['code']; ?>">
<label for="title-<?php echo $lang['code']; ?>">Titel
(<?php echo htmlspecialchars($lang['code']); ?>)</label>
</div>
<div class="input title-<?php echo $lang['code']; ?>">
<input required name="title-<?php echo $lang['code']; ?>"
id="title-<?php echo $lang['code']; ?>" class="text" type="text"
value="<?php echo htmlspecialchars($translations[$lang['code']]['title']); ?>">
</div>
<?php if ($categoryId != 0): ?>
<div class="single_image_group">
<img id="preview-<?= $lang['code'] ?>"
src="<?= $trans['image'] ? '/userdata/' . htmlspecialchars($trans['image']) : '/userdata/intranet/mandant/platzhalter_kategorie.png' ?>"
alt="Bildvorschau" style="max-width: 100px; min-height: 60px;
border-radius: 5px; <?= $trans['image'] ? '' : 'background-color:#f3f3f3;' ?>">
<input type="hidden" name="image-<?= $lang['code'] ?>" id="image-<?= $lang['code'] ?>"
value="<?= htmlspecialchars($trans['image']) ?>">
<div class="post-action-btn green openFileGallery" data-ajax-function="saveCategoryAjax"
data-target="image-<?= $lang['code'] ?>">
+</div>
<div class="post-action-btn clearImage" data-target="image-<?= $lang['code'] ?>">✕</div>
</div>
<?php endif; ?>
</form>
</div>
<?php endforeach; ?>
</div>
<!-- Settings sidebar --> <?php if ($categoryId != 0): ?>
<div class="cf-settings-col"> <div class="category_sub_select">
<label for="stateCheckbox"> <div class="category_sub_select_container">
<?= $translation->get("active_state") ?> <h3><?= $translation->get("connected_subcategories") ?></h3>
<input type="checkbox" id="stateCheckbox" name="state" value="1" <?= $state === 1 ? 'checked' : '' ?>> <input type="search" class="subcategory-search" id="search-selected"
</label> placeholder="Suche in verknüpften Kategorien">
<ul id="selectedSubcategories" class="list-group connectedSortable">
<?php $is_navigation_item = $translations[$languages[0]['code']]['is_navigation_item'] ?? 0; ?> <?php if (empty($subcategories)): ?>
<label for="isNavigationItem"> <li class="list-group-item empty-hint">
<?= $translation->get("create_navigation_menu") ?> <?= $translation->get("no_connected_subcategories") ?>
<input type="checkbox" id="isNavigationItem" name="is_navigation_item" value="1" </li>
<?= $is_navigation_item == 1 ? 'checked' : '' ?>> <?php else: ?>
</label> <?php foreach ($subcategories as $sub): ?>
<li class="list-group-item" data-id="<?= $sub['id'] ?>">
<label for="color_hex"> <div>
<?= $translation->get("color_hex") ?> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
<input type="color" id="color_hex" name="color_hex" value="<?= $category['color_hex'] ?? '#3B82F6FF' ?>"> class="bi bi-grip-vertical" viewBox="0 0 16 16">
</label> <path
d="M7 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0M7 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0M7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0m-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0m-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0" />
<?php $enable_post_color_pickers = isset($category['enable_post_color_pickers']) ? (int) $category['enable_post_color_pickers'] : 0; ?> </svg>
<label for="enablePostColorPickersCheckbox"> <?= htmlspecialchars($sub['title'] ?? "Fehlende Übersetzung ID {$sub['id']}") ?>
<?= $translation->get("enable_post_color_pickers") ?> </div>
<input type="checkbox" id="enablePostColorPickersCheckbox" name="enable_post_color_pickers" value="1" <button title="<?= $translation->get("delete") ?>" type="button"
<?= $enable_post_color_pickers === 1 ? 'checked' : '' ?>> class="unlinkSubcategory">✕</button>
</label>
<?php if (!empty($categoryId)): ?>
<?php
$formatter = new IntlDateFormatter(
'de_DE',
IntlDateFormatter::LONG,
IntlDateFormatter::SHORT
);
$timestamp = strtotime($category["modified_at"]);
?>
<div class="cf-settings-meta">
<small><?= $translation->get("connected_subcategories") ?> (<?= $parentCount ?>)</small>
<?php if ($parentCount > 0): ?>
<ul class="connected_categories">
<?php foreach ($parentCategories as $parent): ?>
<li>
<a target="_blank" href="?action=Categories&detail=<?= $parent['id'] ?>">
<?= htmlspecialchars($parent['title'] ?: "Kategorie-ID " . $parent['id']) ?> ↗
</a>
</li> </li>
<?php endforeach; ?> <?php endforeach; ?>
</ul> <?php endif; ?>
<?php else: ?> </ul>
<p style="font-style: italic;"><?= $translation->get("not_connected_as_subcategory") ?></p>
<?php endif; ?>
<small><?= $translation->get("last_change_on") ?></small>
<p style="font-style: italic;"><?= $formatter->format($timestamp) ?> - <?= $category["modified_by"] ?></p>
</div> </div>
<?php endif; ?>
</div> <div class="category_sub_select_container">
<h3><?= $translation->get("available_categories") ?></h3>
<input type="search" class="subcategory-search" id="search-available"
placeholder="Suche in verfügbaren Kategorien">
<ul id="availableSubcategories" class="list-group connectedSortable">
<?php if (mysqli_num_rows($availableResult) == 0): ?>
<li class="list-group-item empty-hint">
<?= $translation->get("all_available_categories_used") ?>
</li>
<?php else: ?>
<?php while ($row = mysqli_fetch_assoc($availableResult)): ?>
<li class="list-group-item" data-id="<?= $row['id'] ?>">
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-grip-vertical" viewBox="0 0 16 16">
<path
d="M7 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0M7 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0M7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0m-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0m-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0" />
</svg>
<?= htmlspecialchars($row['title'] ?: "Fehlende Übersetzung ID {$row['id']}") ?>
</div>
</li>
<?php endwhile; ?>
<?php endif; ?>
</ul>
</div>
</div>
<?php endif; ?>
</div> </div>
</div> </div>
<?php if ($categoryId != 0): ?> <div class="aside">
<!-- Tab: Berechtigungen & Filter --> <div class="aside_section">
<div id="cf-tab-berechtigungen" class="cf-tab-pane"> <div class="aside_toolbar">
<div class="cf-perm-grid"> <?php if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1) { ?>
<div class="cf-perm-block cf-perm-sm"> <button class="post-action-btn" id="saveAllTranslations" style="background: #14b814;">
<h4><?= $translation->get("mandants") ?></h4> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-check" viewBox="0 0 16 16">
<path
d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z">
</path>
</svg>
</button>
<?php } ?>
<?php if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1) { ?>
<button class="post-action-btn red" id="deleteCategory">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-trash3-fill" viewBox="0 0 16 16">
<path
d="M11 1.5v1h3.5a.5.5 0 0 1 0 1h-.538l-.853 10.66A2 2 0 0 1 11.115 16h-6.23a2 2 0 0 1-1.994-1.84L2.038 3.5H1.5a.5.5 0 0 1 0-1H5v-1A1.5 1.5 0 0 1 6.5 0h3A1.5 1.5 0 0 1 11 1.5m-5 0v1h4v-1a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5M4.5 5.029l.5 8.5a.5.5 0 1 0 .998-.06l-.5-8.5a.5.5 0 1 0-.998.06m6.53-.528a.5.5 0 0 0-.528.47l-.5 8.5a.5.5 0 0 0 .998.058l.5-8.5a.5.5 0 0 0-.47-.528M8 4.5a.5.5 0 0 0-.5.5v8.5a.5.5 0 0 0 1 0V5a.5.5 0 0 0-.5-.5" />
</svg>
</button>
<?php } ?>
</div>
</div>
<div class="aside_section">
<label for="stateCheckbox">
<?= $translation->get("active_state") ?>
<input type="checkbox" id="stateCheckbox" name="state" value="1" <?= $state === 1 ? 'checked' : '' ?>>
</label>
</div>
<?php $is_navigation_item = $translations[$languages[0]['code']]['is_navigation_item'] ?? 0; ?>
<div class="aside_section">
<label for="isNavigationItem">
<?= $translation->get("create_navigation_menu") ?>
<input type="checkbox" id="isNavigationItem" name="is_navigation_item" value="1"
<?= $is_navigation_item == 1 ? 'checked' : '' ?>>
</label>
</div>
<div class="aside_section">
<label for="color_hex">
<?= $translation->get("color_hex") ?>
<input type="color" id="color_hex" name="color_hex" value="<?= $category['color_hex'] ?? '#3B82F6FF' ?>">
</label>
</div>
<?php $enable_post_color_pickers = isset($category['enable_post_color_pickers']) ? (int) $category['enable_post_color_pickers'] : 0; ?>
<div class="aside_section">
<label for="enablePostColorPickersCheckbox">
<?= $translation->get("enable_post_color_pickers") ?>
<input type="checkbox" id="enablePostColorPickersCheckbox" name="enable_post_color_pickers" value="1"
<?= $enable_post_color_pickers === 1 ? 'checked' : '' ?>>
</label>
</div>
<?php $betriebsrat_editable = isset($category['betriebsrat_editable']) ? (int) $category['betriebsrat_editable'] : 0; ?>
<div class="aside_section">
<label for="betriebsratEditableCheckbox">
Für Betriebsrat bearbeitbar
<input type="checkbox" id="betriebsratEditableCheckbox" name="betriebsrat_editable" value="1"
<?= $betriebsrat_editable === 1 ? 'checked' : '' ?>>
</label>
</div>
<?php if (!empty($categoryId)): ?>
<?php
$formatter = new IntlDateFormatter(
'de_DE',
IntlDateFormatter::LONG,
IntlDateFormatter::SHORT
);
$timestamp = strtotime($category["modified_at"]);
?>
<div class="aside_section">
<small><?= $translation->get("connected_subcategories") ?> (<?= $parentCount ?>)</small>
<?php if ($parentCount > 0): ?>
<ul class="connected_categories">
<?php foreach ($parentCategories as $parent): ?>
<li>
<a target="_blank" href="?action=Categories&detail=<?= $parent['id'] ?>">
<?= htmlspecialchars($parent['title'] ?: "Kategorie-ID " . $parent['id']) ?> ↗
</a>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p style="font-style: italic;"><?= $translation->get("not_connected_as_subcategory") ?></p>
<?php endif; ?>
</div>
<div class="aside_section">
<small><?= $translation->get("last_change_on") ?></small>
<p style="font-style: italic;">
<?= $formatter->format($timestamp) ?> - <?= $category["modified_by"] ?>
</p>
</div>
<?php endif; ?>
</div>
</div>
<?php if ($categoryId != 0): ?>
<div class="cardform_containern perm-fullwidth">
<div class="cardform" style="flex:1;">
<div class="card-body">
<h3><?= $translation->get("permissions_and_filters") ?></h3>
<div class="perm-row">
<label for="selected_mandants"><?= $translation->get("mandants") ?></label>
<input type="text" id="selected_mandants" name="selected_mandants" <input type="text" id="selected_mandants" name="selected_mandants"
value='<?= htmlspecialchars(json_encode($savedMandantsData)); ?>'> value='<?= htmlspecialchars(json_encode($savedMandantsData)); ?>'>
</div> </div>
<div class="cf-perm-block cf-perm-sm"> <div class="perm-row">
<h4><?= $translation->get("departments") ?></h4> <label for="selected_departments"><?= $translation->get("departments") ?></label>
<input type="text" id="selected_departments" name="selected_departments" <input type="text" id="selected_departments" name="selected_departments"
value='<?= htmlspecialchars(json_encode($savedDepartmentsData)); ?>'> value='<?= htmlspecialchars(json_encode($savedDepartmentsData)); ?>'>
</div> </div>
<div class="cf-perm-block cf-perm-sm"> <div class="perm-row">
<h4><?= $translation->get("roles") ?></h4> <label for="selected_roles"><?= $translation->get("roles") ?></label>
<input type="text" id="selected_roles" name="selected_roles" <input type="text" id="selected_roles" name="selected_roles"
value='<?= htmlspecialchars(json_encode($savedRolesData)); ?>'> value='<?= htmlspecialchars(json_encode($savedRolesData)); ?>'>
</div> </div>
<div class="cf-perm-block cf-perm-lg"> <div class="perm-row">
<h4>Einrichtungen</h4> <label for="selected_einrichts">Einrichtungen</label>
<input type="text" id="selected_einrichts" name="selected_einrichts" <input type="text" id="selected_einrichts" name="selected_einrichts"
value='<?= htmlspecialchars(json_encode($savedEinrichtsData)); ?>'> value='<?= htmlspecialchars(json_encode($savedEinrichtsData)); ?>'>
</div> </div>
<div class="cf-perm-block cf-perm-lg"> <div class="perm-row">
<h4>Fachbereiche</h4> <label for="selected_fachbereiche">Fachbereiche</label>
<input type="text" id="selected_fachbereiche" name="selected_fachbereiche" <input type="text" id="selected_fachbereiche" name="selected_fachbereiche"
value='<?= htmlspecialchars(json_encode($savedFachbereicheData)); ?>'> value='<?= htmlspecialchars(json_encode($savedFachbereicheData)); ?>'>
</div> </div>
</div> </div>
</div> </div>
<!-- Tab: Unterkategorien -->
<div id="cf-tab-unterkategorien" class="cf-tab-pane">
<div class="category_sub_select">
<div class="category_sub_select_container">
<h3><?= $translation->get("connected_subcategories") ?></h3>
<input type="search" class="subcategory-search" id="search-selected"
placeholder="Suche in verknüpften Kategorien">
<ul id="selectedSubcategories" class="list-group connectedSortable">
<?php if (empty($subcategories)): ?>
<li class="list-group-item empty-hint">
<?= $translation->get("no_connected_subcategories") ?>
</li>
<?php else: ?>
<?php foreach ($subcategories as $sub): ?>
<li class="list-group-item" data-id="<?= $sub['id'] ?>">
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-grip-vertical" viewBox="0 0 16 16">
<path d="M7 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0M7 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0M7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0m-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0m-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
</svg>
<?= htmlspecialchars($sub['title'] ?? "Fehlende Übersetzung ID {$sub['id']}") ?>
</div>
<button title="<?= $translation->get("delete") ?>" type="button"
class="unlinkSubcategory">✕</button>
</li>
<?php endforeach; ?>
<?php endif; ?>
</ul>
</div>
<div class="category_sub_select_container">
<h3><?= $translation->get("available_categories") ?></h3>
<input type="search" class="subcategory-search" id="search-available"
placeholder="Suche in verfügbaren Kategorien">
<ul id="availableSubcategories" class="list-group connectedSortable">
<?php if (mysqli_num_rows($availableResult) == 0): ?>
<li class="list-group-item empty-hint">
<?= $translation->get("all_available_categories_used") ?>
</li>
<?php else: ?>
<?php while ($row = mysqli_fetch_assoc($availableResult)): ?>
<li class="list-group-item" data-id="<?= $row['id'] ?>">
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-grip-vertical" viewBox="0 0 16 16">
<path d="M7 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0M7 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0M7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0m-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0m-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
</svg>
<?= htmlspecialchars($row['title'] ?: "Fehlende Übersetzung ID {$row['id']}") ?>
</div>
</li>
<?php endwhile; ?>
<?php endif; ?>
</ul>
</div>
</div>
</div>
<?php endif; ?>
</div> </div>
<?php endif; ?>
<input type="hidden" id="category_id" value="<?= $categoryId ?>"> <input type="hidden" id="category_id" value="<?= $categoryId ?>">
<input type="hidden" id="modified_by" <input type="hidden" id="modified_by"
@@ -488,15 +501,6 @@ $parentCount = count($parentCategories);
<script> <script>
$(function () { $(function () {
// Section tab switching
$('.cf-section-tab').on('click', function () {
var target = $(this).data('target');
$('.cf-section-tab').removeClass('active');
$(this).addClass('active');
$('.cf-tab-pane').removeClass('active').hide();
$('#' + target).addClass('active').show();
});
// Funktion zum sequentiellen Speichern der Übersetzungen // Funktion zum sequentiellen Speichern der Übersetzungen
window.saveCategoryAjax = function (callback) { window.saveCategoryAjax = function (callback) {
var forms = $(".translationForm"); var forms = $(".translationForm");
@@ -517,7 +521,7 @@ $parentCount = count($parentCategories);
var state = $('#stateCheckbox').is(':checked') ? 1 : 0; var state = $('#stateCheckbox').is(':checked') ? 1 : 0;
var isNavigationItem = $('#isNavigationItem').is(':checked') ? 1 : 0; var isNavigationItem = $('#isNavigationItem').is(':checked') ? 1 : 0;
var enablePostColorPickers = $('#enablePostColorPickersCheckbox').is(':checked') ? 1 : 0; var enablePostColorPickers = $('#enablePostColorPickersCheckbox').is(':checked') ? 1 : 0;
var betriebsratEditable = $('#betriebsratEditableCheckbox').is(':checked') ? 1 : 0;
$.post("/module/knowledgecenter_update/Ajax/Ajax.php", { $.post("/module/knowledgecenter_update/Ajax/Ajax.php", {
action: "update_category_detail", action: "update_category_detail",
@@ -529,7 +533,8 @@ $parentCount = count($parentCategories);
state: state, state: state,
is_navigation_item: isNavigationItem, is_navigation_item: isNavigationItem,
enable_post_color_pickers: enablePostColorPickers, enable_post_color_pickers: enablePostColorPickers,
color_hex: $('#color_hex').val() color_hex: $('#color_hex').val(),
betriebsrat_editable: betriebsratEditable
}, function (response) { }, function (response) {
if (!response.success) { if (!response.success) {
showMessage("Fehler beim Speichern der Übersetzung (" + langCode + "): " + response.error, false); showMessage("Fehler beim Speichern der Übersetzung (" + langCode + "): " + response.error, false);
@@ -844,6 +849,17 @@ $parentCount = count($parentCategories);
} }
} }
}); });
var einrichtLinks = <?= json_encode($einrichtLinks) ?>;
tagifyEinrichts.on('add', function(e) {
var addedId = String(e.detail.data.value);
(einrichtLinks[addedId] || []).forEach(function(lid) {
var exists = tagifyEinrichts.value.some(function(t) { return String(t.value) === lid; });
if (!exists) {
var item = einrichtsData.find(function(d) { return String(d.value) === lid; });
if (item) tagifyEinrichts.addTags([item]);
}
});
});
var tagifyFachbereiche = new Tagify(document.getElementById('selected_fachbereiche'), { var tagifyFachbereiche = new Tagify(document.getElementById('selected_fachbereiche'), {
whitelist: fachbereicheData, whitelist: fachbereicheData,

View File

@@ -26,6 +26,8 @@ if (empty($GLOBALS['allowedRoles'])) {
// Standard-Redirect-URL (zum Beispiel für Navigation) // Standard-Redirect-URL (zum Beispiel für Navigation)
$redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=List'; $redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=List';
$_kcBrOnly = !is_wiki_redakteur() && is_wiki_betriebsrat();
?> ?>
<script> <script>
(function () { (function () {
@@ -38,18 +40,27 @@ $redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=Lis
<form action="" id="<?= $formname = "categories_posts_listform" ?>" name="<?= $formname ?>" method="post"> <form action="" id="<?= $formname = "categories_posts_listform" ?>" name="<?= $formname ?>" method="post">
<div class="titlebar"> <div class="titlebar">
<div class="searchbar"> <div class="searchbar">
<?php if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1) { ?> <?php if (has_wiki_edit_permission()) { ?>
<?= button("post_list", $translation->get("post_list"), $formname, "sendRequest('PostList', true)"); ?> <?= button("post_list", $translation->get("post_list"), $formname, "sendRequest('PostList', true)"); ?>
<?php } ?> <?php } ?>
<?php if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1) { ?> <?php if (is_wiki_redakteur()) { ?>
<?= button("category_list", $translation->get("category_list"), $formname, "sendRequest('Categories', true)"); ?> <?= button("category_list", $translation->get("category_list"), $formname, "sendRequest('Categories', true)"); ?>
<?php } ?> <?php } ?>
<div id="changeCategoryGridView"> <div id="changeCategoryGridView">
</div> </div>
</div> </div>
<div> <div class="titlebar-actions">
<?php if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1) { ?> <?php
<a href="?action=NewPost&InitialCategory=<?= $_GET["detail"] ?>" // "Beitrag erstellen" für Redakteur immer, für Betriebsrat nur in freigegebenen Kategorien
// (inkl. Vererbung: Unterkategorien einer freigegebenen Kategorie gelten ebenfalls)
$showCreateBtn = false;
if (is_wiki_redakteur()) {
$showCreateBtn = true;
} elseif ($_kcBrOnly && isset($_GET['detail'])) {
$showCreateBtn = is_category_betriebsrat_editable((int)$_GET['detail']);
}
if ($showCreateBtn) { ?>
<a href="?action=NewPost&InitialCategory=<?= (int)($_GET["detail"] ?? 0) ?>"
title="<?= $translation->get("new_wiki_post") ?>" role="button" class="green">Beitrag erstellen +</a> title="<?= $translation->get("new_wiki_post") ?>" role="button" class="green">Beitrag erstellen +</a>
<?php } ?> <?php } ?>
@@ -59,8 +70,8 @@ $redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=Lis
<div id="searchResults"></div> <div id="searchResults"></div>
<?php <?php
$placeholderCategory = '/userdata/intranet/mandant/platzhalter_kategorie.png'; $placeholderCategory = '/userdata/01_Basic/platzhalter_bild_kategorie.jpg';
$placeholderPost = '/userdata/intranet/mandant/platzhalter.jpg'; $placeholderPost = '/userdata/01_Basic/platzhalter_bild.jpg';
if (isset($_GET['detail'])) { if (isset($_GET['detail'])) {
// Detailansicht einer Kategorie: // Detailansicht einer Kategorie:
@@ -116,11 +127,12 @@ $redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=Lis
* entweder kein Datensatz vorliegt (NULL) oder ob der Wert in den erlaubten Werten enthalten ist. * entweder kein Datensatz vorliegt (NULL) oder ob der Wert in den erlaubten Werten enthalten ist.
*/ */
$sqlSubs = " $sqlSubs = "
SELECT SELECT
c.id, c.id,
t.title, t.title,
t.description,
t.image t.image
FROM FROM
knowledgecenter_category_links AS kcl knowledgecenter_category_links AS kcl
JOIN JOIN
knowledgecenter_categories_update AS c ON kcl.child_category_id = c.id knowledgecenter_categories_update AS c ON kcl.child_category_id = c.id
@@ -161,6 +173,9 @@ $redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=Lis
$imgSrc = !empty($sub['image']) ? '/userdata/' . htmlspecialchars($sub['image']) : $placeholderCategory; $imgSrc = !empty($sub['image']) ? '/userdata/' . htmlspecialchars($sub['image']) : $placeholderCategory;
echo "<img src='" . $imgSrc . "' alt='Subkategorie Bild'>"; echo "<img src='" . $imgSrc . "' alt='Subkategorie Bild'>";
echo "<div class='card_title'>" . htmlspecialchars($sub['title'] ?: "Keine Übersetzung vorhanden") . "</div>"; echo "<div class='card_title'>" . htmlspecialchars($sub['title'] ?: "Keine Übersetzung vorhanden") . "</div>";
if (!empty($sub['description'])) {
echo "<div class='card_description'>" . htmlspecialchars($sub['description']) . "</div>";
}
echo "</div>"; echo "</div>";
echo "</a>"; echo "</a>";
} }
@@ -171,29 +186,44 @@ $redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=Lis
* gefiltert, sodass in den Bereichen (Abteilung, Mandant, Rolle) entweder kein Eintrag vorliegt * gefiltert, sodass in den Bereichen (Abteilung, Mandant, Rolle) entweder kein Eintrag vorliegt
* oder der Eintrag mit den erlaubten Werten übereinstimmt. * oder der Eintrag mit den erlaubten Werten übereinstimmt.
*/ */
// Für Betriebsrat: nur Beiträge zeigen, die mindestens einer freigegebenen Kategorie zugeordnet sind
$brCategoryFilter = '';
if ($_kcBrOnly) {
$brCatIds = get_all_betriebsrat_accessible_category_ids();
if (empty($brCatIds)) {
$brCategoryFilter = 'AND 1=0'; // kein Zugriff
} else {
$brCatIdList = implode(',', $brCatIds);
$brCategoryFilter = "AND EXISTS (
SELECT 1 FROM knowledgecenter_category_post kcp_br
WHERE kcp_br.post_id = kp.id AND kcp_br.category_id IN ($brCatIdList)
)";
}
}
$sqlPosts = " $sqlPosts = "
SELECT SELECT
kp.id, kp.id,
kp.modified_at, kp.modified_at,
pv.title, pv.title,
pv.image, pv.image,
pv.link, pv.link,
pv.link_label, pv.link_label,
pv.version_number pv.version_number
FROM FROM
knowledgecenter_category_post AS kcp knowledgecenter_category_post AS kcp
JOIN JOIN
knowledgecenter_posts AS kp ON kcp.post_id = kp.id knowledgecenter_posts AS kp ON kcp.post_id = kp.id
LEFT JOIN LEFT JOIN
knowledgecenter_post_versions AS pv ON kp.id = pv.post_id knowledgecenter_post_versions AS pv ON kp.id = pv.post_id
AND pv.language_id = $languageId AND pv.language_id = $languageId
AND pv.state = 1 AND pv.state = 1
AND (pv.valid_from IS NULL OR pv.valid_from <= NOW()) AND (pv.valid_from IS NULL OR pv.valid_from <= NOW())
AND (pv.valid_until IS NULL OR pv.valid_until >= NOW()) AND (pv.valid_until IS NULL OR pv.valid_until >= NOW())
AND pv.version_number = ( AND pv.version_number = (
SELECT MAX(v2.version_number) SELECT MAX(v2.version_number)
FROM knowledgecenter_post_versions AS v2 FROM knowledgecenter_post_versions AS v2
WHERE v2.post_id = kp.id WHERE v2.post_id = kp.id
AND v2.language_id = $languageId AND v2.language_id = $languageId
AND v2.state = 1 AND v2.state = 1
AND (v2.valid_from IS NULL OR v2.valid_from <= NOW()) AND (v2.valid_from IS NULL OR v2.valid_from <= NOW())
@@ -217,9 +247,10 @@ $redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=Lis
AND ( pr.role_id IS NULL OR pr.role_id IN ($allowedRoles) ) AND ( pr.role_id IS NULL OR pr.role_id IN ($allowedRoles) )
AND ( pe.einricht_id IS NULL OR pe.einricht_id IN ($allowedEinrichts) ) AND ( pe.einricht_id IS NULL OR pe.einricht_id IN ($allowedEinrichts) )
AND ( pfb.fachbereich_id IS NULL OR pfb.fachbereich_id IN ($allowedFachbereiche) ) AND ( pfb.fachbereich_id IS NULL OR pfb.fachbereich_id IN ($allowedFachbereiche) )
GROUP BY $brCategoryFilter
GROUP BY
kp.id kp.id
ORDER BY ORDER BY
$orderBy $orderDir $orderBy $orderDir
"; ";
@@ -267,11 +298,12 @@ $redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=Lis
// Übersicht aller Top-Level Kategorien (nicht als Unterkategorie verlinkt) // Übersicht aller Top-Level Kategorien (nicht als Unterkategorie verlinkt)
// mit LEFT JOIN zur Filterung anhand der User-Berechtigungen. // mit LEFT JOIN zur Filterung anhand der User-Berechtigungen.
$sql = " $sql = "
SELECT SELECT
c.id, c.id,
t.title, t.title,
t.description,
t.image t.image
FROM FROM
knowledgecenter_categories_update AS c knowledgecenter_categories_update AS c
LEFT JOIN LEFT JOIN
knowledgecenter_category_translations AS t ON c.id = t.category_id AND t.language_id = $languageId knowledgecenter_category_translations AS t ON c.id = t.category_id AND t.language_id = $languageId
@@ -293,9 +325,9 @@ $redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=Lis
AND ( cr.role_id IS NULL OR cr.role_id IN ($allowedRoles) ) AND ( cr.role_id IS NULL OR cr.role_id IN ($allowedRoles) )
AND ( ce.einricht_id IS NULL OR ce.einricht_id IN ($allowedEinrichts) ) AND ( ce.einricht_id IS NULL OR ce.einricht_id IN ($allowedEinrichts) )
AND ( cf.fachbereich_id IS NULL OR cf.fachbereich_id IN ($allowedFachbereiche) ) AND ( cf.fachbereich_id IS NULL OR cf.fachbereich_id IN ($allowedFachbereiche) )
GROUP BY GROUP BY
c.id c.id
ORDER BY ORDER BY
c.sorting ASC c.sorting ASC
"; ";
// var_dump($sql); // var_dump($sql);
@@ -305,11 +337,14 @@ $redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=Lis
$route = isset($_GET['route']) ? $_GET['route'] : ''; $route = isset($_GET['route']) ? $_GET['route'] : '';
$new_route = $route ? $route . ',' . $row['id'] : $row['id']; $new_route = $route ? $route . ',' . $row['id'] : $row['id'];
echo "<a href=\"?detail={$row['id']}&route={$new_route}\">"; echo "<a class='post_wiki category_post' href=\"?detail={$row['id']}&route={$new_route}\">";
echo "<div class='category'>"; echo "<div class='category'>";
$imgSrc = !empty($row['image']) ? '/userdata/' . htmlspecialchars($row['image']) : $placeholderCategory; $imgSrc = !empty($row['image']) ? '/userdata/' . htmlspecialchars($row['image']) : $placeholderCategory;
echo "<img src='" . $imgSrc . "' alt='Kategorie Bild'>"; echo "<img src='" . $imgSrc . "' alt='Kategorie Bild'>";
echo "<div class='card_title'>" . htmlspecialchars($row['title'] ?: "Keine Übersetzung vorhanden") . "</div>"; echo "<div class='card_title'>" . htmlspecialchars($row['title'] ?: "Keine Übersetzung vorhanden") . "</div>";
if (!empty($row['description'])) {
echo "<div class='card_description'>" . htmlspecialchars($row['description']) . "</div>";
}
echo "</div>"; echo "</div>";
echo "</a>"; echo "</a>";
} }

View File

@@ -148,15 +148,19 @@ $initialCategoryId = isset($_GET['InitialCategory']) ? (int) $_GET['InitialCateg
$InitialProduct = isset($_GET['InitialProduct']) ? (int) $_GET['InitialProduct'] : 0; $InitialProduct = isset($_GET['InitialProduct']) ? (int) $_GET['InitialProduct'] : 0;
if ($isNew) { if ($isNew) {
require_permission('r-wiki'); require_wiki_edit_permission();
// Neuer Beitrag: Lege leere bzw. Standardwerte an // Betriebsrat darf nur in Kategorien erstellen, die für ihn freigegeben sind (inkl. Vererbung)
if (!is_wiki_redakteur() && $initialCategoryId > 0) {
if (!is_category_betriebsrat_editable($initialCategoryId)) {
echo "<p>Sie haben nicht die Berechtigung, in dieser Kategorie zu erstellen.</p>";
exit;
}
}
$canEditPost = true; // Berechtigung schon oben geprüft
$postId = 0; $postId = 0;
$post = [ $post = ['id' => 0];
'id' => 0,
// Weitere Felder kannst du hier ggf. ergänzen
];
$commentCount = 0;
$versionCount = 0; $versionCount = 0;
$currentVersion = [ $currentVersion = [
'id' => 0, 'id' => 0,
@@ -179,6 +183,20 @@ if ($isNew) {
echo "<p>Sie haben nicht die notwendigen Berechtigungen.</p>"; echo "<p>Sie haben nicht die notwendigen Berechtigungen.</p>";
exit; exit;
} }
// Schreibrechte: Redakteur darf alles; Betriebsrat nur in freigegebenen Kategorien (inkl. Vererbung)
$canEditPost = is_wiki_redakteur();
if (!$canEditPost && is_wiki_betriebsrat()) {
$brCatRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT category_id FROM knowledgecenter_category_post WHERE post_id = $postId");
$canEditPost = false;
while ($brCatRow = mysqli_fetch_assoc($brCatRes)) {
if (is_category_betriebsrat_editable((int)$brCatRow['category_id'])) {
$canEditPost = true;
break;
}
}
}
// Beitrag aus der Haupttabelle abrufen // Beitrag aus der Haupttabelle abrufen
$sqlPost = "SELECT * FROM knowledgecenter_posts WHERE id = $postId LIMIT 1"; $sqlPost = "SELECT * FROM knowledgecenter_posts WHERE id = $postId LIMIT 1";
$resPost = mysqli_query($GLOBALS['mysql_con'], $sqlPost); $resPost = mysqli_query($GLOBALS['mysql_con'], $sqlPost);
@@ -199,16 +217,6 @@ if ($isNew) {
} }
// Anzahl Kommentare ermitteln
$sqlCountComments = "SELECT COUNT(*) AS commentCount FROM topic WHERE knowledgecenter_post_id = $postId";
$resCountComments = mysqli_query($GLOBALS['mysql_con'], $sqlCountComments);
if ($resCountComments && mysqli_num_rows($resCountComments) > 0) {
$rowCount = mysqli_fetch_assoc($resCountComments);
$commentCount = (int) $rowCount['commentCount'];
} else {
$commentCount = 0;
}
// Anzahl Versionen für die aktuell ausgewählte Sprache ermitteln // Anzahl Versionen für die aktuell ausgewählte Sprache ermitteln
$sqlCountVersions = "SELECT COUNT(*) AS versionCount FROM knowledgecenter_post_versions WHERE post_id = $postId AND language_id = $languageId"; $sqlCountVersions = "SELECT COUNT(*) AS versionCount FROM knowledgecenter_post_versions WHERE post_id = $postId AND language_id = $languageId";
$resCountVersions = mysqli_query($GLOBALS['mysql_con'], $sqlCountVersions); $resCountVersions = mysqli_query($GLOBALS['mysql_con'], $sqlCountVersions);
@@ -374,7 +382,7 @@ if ($resLinkedForm && mysqli_num_rows($resLinkedForm) > 0) {
<a href="#tab-files"><?= $translation->get("files") ?> (<?php echo $fileCount; ?>)</a> <a href="#tab-files"><?= $translation->get("files") ?> (<?php echo $fileCount; ?>)</a>
<a href="#tab-contacts"><?= $translation->get("Ansprechpartner") ?> (<?php echo $contactCount; ?>)</a> <a href="#tab-contacts"><?= $translation->get("Ansprechpartner") ?> (<?php echo $contactCount; ?>)</a>
<a href="#tab-versions"><?= $translation->get("version_history") ?> (<?php echo $versionCount; ?>)</a> <a href="#tab-versions"><?= $translation->get("version_history") ?> (<?php echo $versionCount; ?>)</a>
<?php if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1) { ?> <?php if ($canEditPost ?? false) { ?>
<a href="#tab-settings"><?= $translation->get("settings") ?></a> <a href="#tab-settings"><?= $translation->get("settings") ?></a>
<?php } ?> <?php } ?>
</div> </div>
@@ -442,7 +450,7 @@ if ($resLinkedForm && mysqli_num_rows($resLinkedForm) > 0) {
require_once 'post_cardform_version_history.php'; require_once 'post_cardform_version_history.php';
?> ?>
</div> </div>
<?php if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1) { ?> <?php if ($canEditPost ?? false) { ?>
<div id="tab-settings" class="tab-content"> <div id="tab-settings" class="tab-content">
<?php <?php
require_once 'post_cardform_settings.php'; require_once 'post_cardform_settings.php';

View File

@@ -121,14 +121,9 @@
<h1 id="displayTitle"><?php echo htmlspecialchars($currentVersion['title']); ?></h1> <h1 id="displayTitle"><?php echo htmlspecialchars($currentVersion['title']); ?></h1>
<?php endif; ?> <?php endif; ?>
<div> <div>
<?php if (!empty($postId) && $postId > 0): ?> <?php if (!empty($postId) && $postId > 0 && ($canEditPost ?? false)): ?>
<?php if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1): ?> <span id="editBtn" class="post-action-btn grey" title="Aktuelle Version bearbeiten">✎</span>
<span id="editBtn" class="post-action-btn grey" title="Aktuelle Version bearbeiten">✎</span> <span id="createBtn" class="green" title="Neue Version erstellen">Neue Version erstellen +</span>
<?php endif; ?>
<?php if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1): ?>
<span id="createBtn" class="green" title="Neue Version erstellen">Neue Version erstellen +</span>
<?php endif; ?>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> </div>
@@ -211,6 +206,15 @@
</div> </div>
<?php } ?> <?php } ?>
<?php if (!empty($currentVersion['summary'])): ?>
<div class="post-aside-section post-summary-section">
<h3>Zusammenfassung</h3>
<div class="post-summary">
<p><?= nl2br(htmlspecialchars($currentVersion['summary'])) ?></p>
</div>
</div>
<?php endif; ?>
<div id="filesContainer"></div> <div id="filesContainer"></div>
<div id="contactsContainer"></div> <div id="contactsContainer"></div>

View File

@@ -34,7 +34,7 @@
<div class="edit-main"> <div class="edit-main">
<?php foreach ($allLanguages as $lang): <?php foreach ($allLanguages as $lang):
$langId = $lang['id']; $langId = $lang['id'];
$trans = isset($translations[$langId]) ? $translations[$langId] : ['title' => '', 'image' => '', 'teaser' => '', 'content' => '']; $trans = isset($translations[$langId]) ? $translations[$langId] : ['title' => '', 'image' => '', 'teaser' => '', 'content' => '', 'summary' => ''];
?> ?>
<div class="lang-form" id="lang_<?= $langId ?>" data-lang="<?= $langId ?>" <div class="lang-form" id="lang_<?= $langId ?>" data-lang="<?= $langId ?>"
style="<?= ($langId == $languageId) ? '' : 'display: none;' ?>"> style="<?= ($langId == $languageId) ? '' : 'display: none;' ?>">
@@ -65,13 +65,17 @@
<div id="editor_<?= $langId ?>" class="editor"> <div id="editor_<?= $langId ?>" class="editor">
<?= $translations[$langId]['content'] ?> <?= $translations[$langId]['content'] ?>
</div> </div>
<hr>
<label for="summary_<?= $langId ?>">Zusammenfassung (Summary)</label>
<textarea id="summary_<?= $langId ?>" name="summary[<?= $langId ?>]" rows="4"
placeholder="Kurze Zusammenfassung des Artikelinhalts..."><?= htmlspecialchars($trans['summary'] ?? '') ?></textarea>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<!-- Right column: Sidebar with save button and meta fields --> <!-- Right column: Sidebar with save button and meta fields -->
<div class="edit-sidebar"> <div class="edit-sidebar">
<?php if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1 || get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1) { ?> <?php if ($canEditPost ?? false) { ?>
<span title="<?= $translation->get("save") ?>" id="saveVersionBtn" class="post-action-btn-text green"> <span title="<?= $translation->get("save") ?>" id="saveVersionBtn" class="post-action-btn-text green">
<?= $translation->get("save") ?> <?= $translation->get("save") ?>
</span> </span>
@@ -80,7 +84,7 @@
<?php foreach ($allLanguages as $lang): <?php foreach ($allLanguages as $lang):
$langId = $lang['id']; $langId = $lang['id'];
$trans = isset($translations[$langId]) ? $translations[$langId] : ['title' => '', 'image' => '', 'teaser' => '', 'content' => '']; $trans = isset($translations[$langId]) ? $translations[$langId] : ['title' => '', 'image' => '', 'teaser' => '', 'content' => '', 'summary' => ''];
?> ?>
<div class="lang-form-meta" data-lang="<?= $langId ?>" <div class="lang-form-meta" data-lang="<?= $langId ?>"
style="<?= ($langId == $languageId) ? '' : 'display: none;' ?>"> style="<?= ($langId == $languageId) ? '' : 'display: none;' ?>">

View File

@@ -1,6 +1,6 @@
<div class="titlebar_small"> <div class="titlebar_small">
<h2><?= $translation->get("files") ?></h2> <h2><?= $translation->get("files") ?></h2>
<?php if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1) { ?> <?php if (($canEditPost ?? false)) { ?>
<div class="post-action-btn-small green openFileGalleryMulti" title="Datei hinzufügen" <div class="post-action-btn-small green openFileGalleryMulti" title="Datei hinzufügen"
data-ajax-function="saveFilesAjax" data-target="post_files">+ data-ajax-function="saveFilesAjax" data-target="post_files">+
</div> </div>
@@ -111,7 +111,7 @@
} }
// Definiere den Lösch-Button // Definiere den Lösch-Button
<?php if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1) { ?> <?php if (($canEditPost ?? false)) { ?>
var deleteButton = "<span class='file-delete post-action-btn-small' data-file-id='" + file.id + "' onclick='deleteFile(" + file.id + ")'>✕</span>"; var deleteButton = "<span class='file-delete post-action-btn-small' data-file-id='" + file.id + "' onclick='deleteFile(" + file.id + ")'>✕</span>";
<?php } else { <?php } else {
?> ?>

View File

@@ -36,15 +36,19 @@ while ($row = mysqli_fetch_assoc($result)) {
// -------------------- Kategorien -------------------- // -------------------- Kategorien --------------------
// Erzeuge die Whitelist für alle verfügbaren Kategorien (mit Übersetzung) // Erzeuge die Whitelist für alle verfügbaren Kategorien (mit Übersetzung)
// Betriebsrat sieht nur Kategorien, die für ihn freigegeben sind (direkt oder über Elternkategorie)
$categoriesData = []; $categoriesData = [];
$query = "SELECT c.id, t.title, c.enable_post_color_pickers $query = "SELECT c.id, t.title, c.enable_post_color_pickers
FROM knowledgecenter_categories_update AS c FROM knowledgecenter_categories_update AS c
LEFT JOIN knowledgecenter_category_translations AS t LEFT JOIN knowledgecenter_category_translations AS t
ON c.id = t.category_id ON c.id = t.category_id
WHERE t.language_id = $languageId"; WHERE t.language_id = $languageId";
$result = mysqli_query($GLOBALS['mysql_con'], $query); $result = mysqli_query($GLOBALS['mysql_con'], $query);
$isBrOnly = !is_wiki_redakteur() && is_wiki_betriebsrat();
while ($row = mysqli_fetch_assoc($result)) { while ($row = mysqli_fetch_assoc($result)) {
// Hier wird der in der Übersetzungstabelle gespeicherte Titel als 'name' verwendet. if ($isBrOnly && !is_category_betriebsrat_editable((int)$row['id'])) {
continue;
}
$categoriesData[] = [ $categoriesData[] = [
'value' => $row['id'], 'value' => $row['id'],
'name' => $row['title'], 'name' => $row['title'],
@@ -132,6 +136,13 @@ $result = mysqli_query($GLOBALS['mysql_con'], $query);
while ($row = mysqli_fetch_assoc($result)) { while ($row = mysqli_fetch_assoc($result)) {
$postEinrichtsData[] = ['value' => $row['id'], 'name' => $row['description']]; $postEinrichtsData[] = ['value' => $row['id'], 'name' => $row['description']];
} }
// Verknüpfungskarte für Einrichtungen (Gruppen → Mitglieder)
$postEinrichtLinks = [];
$linkRes = mysqli_query($GLOBALS['mysql_con'], "SELECT einricht_id, linked_einricht_id FROM organigramm_einricht_link");
while ($lr = mysqli_fetch_assoc($linkRes)) {
$postEinrichtLinks[strval($lr['einricht_id'])][] = strval($lr['linked_einricht_id']);
}
$selectedPostEinrichts = []; $selectedPostEinrichts = [];
if ($postId != 0) { if ($postId != 0) {
$querySelected = "SELECT e.id, e.description $querySelected = "SELECT e.id, e.description
@@ -185,7 +196,7 @@ if (!empty($post['text_color'])) {
<button id="message" style="opacity:0; position: absolute;"></button> <button id="message" style="opacity:0; position: absolute;"></button>
<div class="titlebar_small"> <div class="titlebar_small">
<h2><?=$translation->get("settings")?></h2> <h2><?=$translation->get("settings")?></h2>
<?php if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1) { ?> <?php if ($canEditPost ?? false) { ?>
<span id="saveAllBtn" class="post-action-btn-small green"><?= $saveIcon ?></span> <span id="saveAllBtn" class="post-action-btn-small green"><?= $saveIcon ?></span>
<?php } ?> <?php } ?>
</div> </div>
@@ -405,6 +416,17 @@ if (!empty($post['text_color'])) {
if (selectedPostEinrichts.length) { if (selectedPostEinrichts.length) {
tagifyPostEinrichts.addTags(selectedPostEinrichts); tagifyPostEinrichts.addTags(selectedPostEinrichts);
} }
var postEinrichtLinks = <?= json_encode($postEinrichtLinks) ?>;
tagifyPostEinrichts.on('add', function(e) {
var addedId = String(e.detail.data.value);
(postEinrichtLinks[addedId] || []).forEach(function(lid) {
var exists = tagifyPostEinrichts.value.some(function(t) { return String(t.value) === lid; });
if (!exists) {
var item = postEinrichtsData.find(function(d) { return String(d.value) === lid; });
if (item) tagifyPostEinrichts.addTags([item]);
}
});
});
// Fachbereiche für Post // Fachbereiche für Post
var tagifyPostFachbereiche = new Tagify(document.getElementById('postFachbereichTags'), { var tagifyPostFachbereiche = new Tagify(document.getElementById('postFachbereichTags'), {
@@ -424,7 +446,7 @@ if (!empty($post['text_color'])) {
tagifyPostFachbereiche.addTags(selectedPostFachbereiche); tagifyPostFachbereiche.addTags(selectedPostFachbereiche);
} }
<?php if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1) { ?> <?php if ($canEditPost ?? false) { ?>
$("#saveAllBtn").on("click", function () { $("#saveAllBtn").on("click", function () {
// Während des Speicherns blurren // Während des Speicherns blurren
$("#tab-settings").addClass("blurred"); $("#tab-settings").addClass("blurred");

View File

@@ -3,14 +3,32 @@
// Redirect-URL für den Zurück-Button bzw. Navigation // Redirect-URL für den Zurück-Button bzw. Navigation
$redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=Category'; $redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=Category';
// Kategorien für das Dropdown holen (state = 1) // Kategorien für das Dropdown holen
$catsQuery = " $_isBrOnly = !is_wiki_redakteur() && is_wiki_betriebsrat();
SELECT c.id, t.title if ($_isBrOnly) {
FROM knowledgecenter_categories_update AS c $_brCatIds = get_all_betriebsrat_accessible_category_ids();
LEFT JOIN knowledgecenter_category_translations AS t if (empty($_brCatIds)) {
ON t.category_id = c.id AND t.language_id = $languageId $catsQuery = "SELECT NULL LIMIT 0";
ORDER BY t.title ASC } else {
"; $_brCatIdList = implode(',', $_brCatIds);
$catsQuery = "
SELECT c.id, t.title
FROM knowledgecenter_categories_update AS c
LEFT JOIN knowledgecenter_category_translations AS t
ON t.category_id = c.id AND t.language_id = $languageId
WHERE c.id IN ($_brCatIdList)
ORDER BY t.title ASC
";
}
} else {
$catsQuery = "
SELECT c.id, t.title
FROM knowledgecenter_categories_update AS c
LEFT JOIN knowledgecenter_category_translations AS t
ON t.category_id = c.id AND t.language_id = $languageId
ORDER BY t.title ASC
";
}
$catsResult = mysqli_query($GLOBALS['mysql_con'], $catsQuery); $catsResult = mysqli_query($GLOBALS['mysql_con'], $catsQuery);
$allCategories = []; $allCategories = [];
while ($r = mysqli_fetch_assoc($catsResult)) { while ($r = mysqli_fetch_assoc($catsResult)) {
@@ -44,9 +62,14 @@ while ($r = mysqli_fetch_assoc($catsResult)) {
</div> </div>
<?php
$_brCategoryIds = $_isBrOnly ? ($_brCatIds ?? []) : null;
$_brCategoryIdsJson = json_encode($_brCategoryIds);
?>
<script> <script>
var myRedirectURL = window.location.href; var myRedirectURL = window.location.href;
var debounceTimer; var debounceTimer;
var brCategoryIds = <?= $_brCategoryIdsJson ?>;
function loadPostList() { function loadPostList() {
var searchQuery = $('#searchQuery').val(); var searchQuery = $('#searchQuery').val();
@@ -61,7 +84,8 @@ while ($r = mysqli_fetch_assoc($catsResult)) {
language_id: '<?= $languageId ?>', language_id: '<?= $languageId ?>',
redirectURL: myRedirectURL, redirectURL: myRedirectURL,
query: searchQuery, query: searchQuery,
category_id: categoryId // hier mitgeben category_id: categoryId,
br_category_ids: brCategoryIds !== null ? JSON.stringify(brCategoryIds) : null
}, },
dataType: 'json', dataType: 'json',
beforeSend: function () { $('#overlayLoader').show(); }, beforeSend: function () { $('#overlayLoader').show(); },

View File

@@ -0,0 +1,59 @@
/* Drag-Handle Overlay für Quill Image Resize */
.ql-image-overlay {
box-sizing: border-box;
border: 1px dashed #2684ff;
pointer-events: none;
}
.ql-image-handle {
position: absolute;
width: 12px;
height: 12px;
background: #ffffff;
border: 2px solid #2684ff;
border-radius: 2px;
pointer-events: auto;
z-index: 10001;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
}
.ql-image-handle:hover {
background: #2684ff;
}
.ql-image-handle-nw {
left: -6px;
top: -6px;
cursor: nwse-resize;
}
.ql-image-handle-ne {
right: -6px;
top: -6px;
cursor: nesw-resize;
}
.ql-image-handle-sw {
left: -6px;
bottom: -6px;
cursor: nesw-resize;
}
.ql-image-handle-se {
right: -6px;
bottom: -6px;
cursor: nwse-resize;
}
/* Während des Drags Text-Selection global unterdrücken */
.ql-image-overlay.is-dragging,
.ql-image-overlay.is-dragging * {
user-select: none;
-webkit-user-select: none;
}
/* Im Editor selbst soll das Bild visuell ein leichtes Outline bekommen,
wenn es aktiv ist — aber CSS kann nicht aus dem Overlay zurückgreifen,
daher: ein dünner Schatten ist genug; der gestrichelte Rand am Overlay
markiert das aktive Bild klar genug. */

View File

@@ -0,0 +1,363 @@
/**
* Quill-Modul: Drag-Resize für Bilder im Editor.
*
* Aktivierung pro Editor-Instanz via:
* modules: { imageResize: true }
*
* Verhalten:
* - Click auf <img> im Editor → 4 Ecken-Handles als position:fixed-Overlay
* - mousedown/touchstart auf einem Handle startet den Drag
* - Drag aktualisiert width-Attribut + Inline-Style mit !important am <img>
* (Inline-Style ist nötig, weil die globale `img { max-width:100% }`-Regel
* im Intranet das HTML-width-Attribut überstimmt — Spezifität 0 verliert
* gegen jede Author-CSS-Regel auf <img>)
* - Aspect Ratio über CSS `height: auto` gehalten
*
* Persistenz-Pipeline:
* 1) ResizableImage-Blot (unten registriert) whitelisted alt/width/height in
* Quills formats/image-Override, damit die Attribute den Quill-Save-Pfad
* (`quill.update`) überleben.
* 2) Pro Quill-Instanz: Clipboard-Matcher für IMG + Monkey-Patch um
* `dangerouslyPasteHTML`. Die reparieren den Lade-Pfad, weil Quill
* 2.0.0-dev.3 beim HTML→Delta-Parse die Attribute trotz Blot droppt.
* Der Monkey-Patch reappliziert width-Attribut UND Inline-!important-Style
* am Editor-DOM (gleiches Pattern wie Live-Drag).
*
* Bewusst klein gehalten. Keine Abhängigkeit zu third-party Resize-Modulen,
* die für Quill 1.3.x geschrieben wurden und in 2.0.0-dev.3 unstet sind.
*/
(function (global) {
'use strict';
if (typeof Quill === 'undefined') {
console.error('[quill_resize] Quill ist nicht geladen.');
return;
}
// ResizableImage-Blot: Quill 2.0.0-dev.3' nativer formats/image strippt
// beim Re-Parse alle Attribute außer src. Dieser Blot whitelisted alt,
// width, height, damit die per Drag gesetzte Größe den Roundtrip überlebt.
//
// Quill 2.x' Blots sind ES6-Klassen — Vererbung MUSS via `class ... extends`
// erfolgen, nicht via Prototype-Chain (Image.apply würde TypeError werfen,
// weil ES6-Classes nicht ohne `new` aufgerufen werden können).
//
// try/catch: Falls Quill in einer Version vorliegt, die diese Override-API
// nicht unterstützt, kippt der Editor nicht — wir loggen und fallen
// stillschweigend auf den nativen Image-Blot zurück.
if (!global.__ResizableImageRegistered) {
try {
var Image = Quill.import('formats/image');
var ATTRS = ['alt', 'width', 'height'];
var ResizableImage = class extends Image {
static create(value) {
var src = value;
var extras = null;
if (value && typeof value === 'object') {
src = value.src || value.url || '';
extras = value;
}
var node = super.create(src);
if (extras) {
ATTRS.forEach(function (attr) {
if (extras[attr] !== undefined && extras[attr] !== null && extras[attr] !== '') {
node.setAttribute(attr, extras[attr]);
}
});
}
return node;
}
static formats(domNode) {
var base = (typeof super.formats === 'function') ? super.formats(domNode) : {};
ATTRS.forEach(function (attr) {
if (domNode.hasAttribute(attr)) {
base[attr] = domNode.getAttribute(attr);
}
});
return base;
}
format(name, value) {
if (ATTRS.indexOf(name) > -1) {
if (value) {
this.domNode.setAttribute(name, value);
} else {
this.domNode.removeAttribute(name);
}
} else {
super.format(name, value);
}
}
};
ResizableImage.blotName = 'image';
ResizableImage.tagName = 'IMG';
Quill.register(ResizableImage, true);
global.__ResizableImageRegistered = true;
global.ResizableImage = ResizableImage;
} catch (err) {
console.error('[quill_resize] ResizableImage-Blot konnte nicht registriert werden — fallback auf nativen Image-Blot. Resize-Größe geht beim Re-Edit verloren.', err);
}
}
var MIN_WIDTH = 50;
function ImageResize(quill, options) {
this.quill = quill;
this.editor = quill.root;
this.options = options || {};
this.activeImg = null;
this.overlay = null;
this.dragState = null;
// Methoden binden, damit removeEventListener funktioniert
this.onEditorClick = this.onEditorClick.bind(this);
this.onDocPointerDown = this.onDocPointerDown.bind(this);
this.onReposition = this.onReposition.bind(this);
this.onMove = this.onMove.bind(this);
this.onUp = this.onUp.bind(this);
this.editor.addEventListener('click', this.onEditorClick);
document.addEventListener('mousedown', this.onDocPointerDown, true);
document.addEventListener('touchstart', this.onDocPointerDown, true);
window.addEventListener('resize', this.onReposition);
window.addEventListener('scroll', this.onReposition, true);
// Bei Text-Changes Position anpassen (z.B. wenn Bild verschoben wird)
quill.on('text-change', this.onReposition);
// Quill 2.0.0-dev.3 strippt beim dangerouslyPasteHTML alle <img>-Attribute
// außer src. Reader rendert das gespeicherte HTML direkt — also korrekt;
// aber im Editor ist die Größe nach Re-Open weg. Zwei Schutzschichten:
//
// (a) Clipboard-Matcher: Beim HTML→Delta-Parse die width/height/alt
// als delta.attributes mitführen. Bei korrekt greifendem
// ResizableImage-Blot überlebt das den Render zurück ans DOM.
//
// (b) Monkey-Patch um dangerouslyPasteHTML: Falls (a) in dev.3 nicht
// durchgereicht wird (Embed-format-Hook ignoriert), reapplizieren
// wir die Attribute nach dem Paste direkt am Editor-DOM. Idempotent
// pro Quill-Instanz via Marker-Property.
try {
quill.clipboard.addMatcher('IMG', function (node, delta) {
if (!node || !delta || !delta.ops || !delta.ops.length) return delta;
var op = delta.ops[0];
if (!op || !op.insert) return delta;
var attrs = op.attributes || {};
['alt', 'width', 'height'].forEach(function (a) {
if (node.hasAttribute(a) && attrs[a] == null) {
attrs[a] = node.getAttribute(a);
}
});
if (Object.keys(attrs).length) op.attributes = attrs;
return delta;
});
} catch (e) {
console.warn('[quill_resize] Clipboard-Matcher Setup fehlgeschlagen', e);
}
if (!quill.__pasteHtmlPatched && quill.clipboard
&& typeof quill.clipboard.dangerouslyPasteHTML === 'function') {
var origPaste = quill.clipboard.dangerouslyPasteHTML;
var PASTE_ATTRS = ['alt', 'width', 'height'];
quill.clipboard.dangerouslyPasteHTML = function () {
var args = Array.prototype.slice.call(arguments);
var htmlArg = (typeof args[0] === 'string') ? args[0] : args[1];
var seq = [];
if (typeof htmlArg === 'string' && htmlArg.indexOf('<img') !== -1) {
var tmp = document.createElement('div');
tmp.innerHTML = htmlArg;
Array.prototype.forEach.call(tmp.querySelectorAll('img'), function (img) {
var entry = { src: img.getAttribute('src') || '', attrs: {} };
PASTE_ATTRS.forEach(function (a) {
if (img.hasAttribute(a)) entry.attrs[a] = img.getAttribute(a);
});
seq.push(entry);
});
}
var result = origPaste.apply(this, args);
if (seq.length) {
var domImgs = quill.root.querySelectorAll('img');
var ix = 0;
for (var di = 0; di < domImgs.length && ix < seq.length; di++) {
var dimg = domImgs[di];
var dsrc = dimg.getAttribute('src') || '';
while (ix < seq.length && seq[ix].src !== dsrc) ix++;
if (ix < seq.length && seq[ix].src === dsrc) {
var entryAttrs = seq[ix].attrs;
Object.keys(entryAttrs).forEach(function (k) {
if (!dimg.hasAttribute(k)) {
dimg.setAttribute(k, entryAttrs[k]);
}
});
// Inline-Style mit !important: das HTML-width-Attribut
// hat Spezifität 0 und wird im Intranet von der globalen
// mysyde.css-img-Regel überstimmt (oder von Quills internem
// Embed-Wrapping). Inline-!important gewinnt garantiert.
// Spiegelt das Verhalten von onMove (live-drag) wider.
if (entryAttrs.width) {
dimg.style.setProperty('width', entryAttrs.width + 'px', 'important');
dimg.style.setProperty('height', 'auto', 'important');
}
ix++;
}
}
}
return result;
};
quill.__pasteHtmlPatched = true;
}
}
ImageResize.prototype.onEditorClick = function (e) {
if (e.target && e.target.tagName === 'IMG' && this.editor.contains(e.target)) {
this.attachTo(e.target);
} else {
this.detach();
}
};
ImageResize.prototype.attachTo = function (img) {
if (this.activeImg === img && this.overlay) return;
this.detach();
this.activeImg = img;
this.overlay = this.buildOverlay();
document.body.appendChild(this.overlay);
this.reposition();
};
ImageResize.prototype.buildOverlay = function () {
var self = this;
var box = document.createElement('div');
box.className = 'ql-image-overlay';
['nw', 'ne', 'sw', 'se'].forEach(function (corner) {
var h = document.createElement('div');
h.className = 'ql-image-handle ql-image-handle-' + corner;
h.dataset.corner = corner;
// Corner explizit über Closure binden, nicht über e.currentTarget
// (currentTarget kann nach Listener-Return null sein, je nach Browser).
var startDrag = function (e) { self.onHandleDown(e, corner); };
h.addEventListener('mousedown', startDrag);
h.addEventListener('touchstart', startDrag, { passive: false });
box.appendChild(h);
});
return box;
};
ImageResize.prototype.reposition = function () {
if (!this.activeImg || !this.overlay) return;
var r = this.activeImg.getBoundingClientRect();
var s = this.overlay.style;
s.position = 'fixed';
s.left = r.left + 'px';
s.top = r.top + 'px';
s.width = r.width + 'px';
s.height = r.height + 'px';
// KEIN pointer-events:none hier — die Handles brauchen Klick-Empfang.
// Der gestrichelte Rand des Overlays liegt im CSS auf
// `.ql-image-overlay` mit pointer-events:none direkt im CSS.
s.zIndex = '10000';
};
ImageResize.prototype.onReposition = function () {
this.reposition();
};
ImageResize.prototype.onHandleDown = function (e, corner) {
if (!this.activeImg) return;
e.preventDefault();
e.stopPropagation();
var p = (e.touches && e.touches[0]) ? e.touches[0] : e;
var r = this.activeImg.getBoundingClientRect();
this.dragState = {
startX: p.clientX,
startY: p.clientY,
startW: r.width,
startH: r.height,
corner: corner
};
document.addEventListener('mousemove', this.onMove);
document.addEventListener('mouseup', this.onUp);
document.addEventListener('touchmove', this.onMove, { passive: false });
document.addEventListener('touchend', this.onUp);
document.addEventListener('touchcancel', this.onUp);
};
ImageResize.prototype.onMove = function (e) {
if (!this.dragState || !this.activeImg) return;
if (e.cancelable) e.preventDefault();
var p = (e.touches && e.touches[0]) ? e.touches[0] : e;
var dx = p.clientX - this.dragState.startX;
// Links liegende Handles invertieren das Vorzeichen:
// rechts ziehen → dx positiv → image schrumpft (left handle)
if (this.dragState.corner === 'nw' || this.dragState.corner === 'sw') {
dx = -dx;
}
var editorR = this.editor.getBoundingClientRect();
var maxW = Math.max(MIN_WIDTH, editorR.width - 8);
var newW = Math.max(MIN_WIDTH, Math.min(maxW, this.dragState.startW + dx));
newW = Math.round(newW);
// width sowohl als HTML-Attribut als auch als Inline-Style setzen.
// Inline-Style ist nötig, weil im Intranet eine CSS-Regel mit höherer
// Spezifität als das Attribut existiert (an dieser Stelle nicht zu
// finden ohne Live-DevTools). Inline-Spezifität (1000) gewinnt gegen
// alle nicht-!important CSS-Regeln. setProperty mit 'important' als
// Fallback, falls eine !important-Regel im Spiel ist.
var widthPx = newW + 'px';
this.activeImg.setAttribute('width', String(newW));
this.activeImg.style.setProperty('width', widthPx, 'important');
this.activeImg.style.setProperty('height', 'auto', 'important');
this.activeImg.removeAttribute('height');
this.reposition();
};
ImageResize.prototype.onUp = function () {
document.removeEventListener('mousemove', this.onMove);
document.removeEventListener('mouseup', this.onUp);
document.removeEventListener('touchmove', this.onMove);
document.removeEventListener('touchend', this.onUp);
document.removeEventListener('touchcancel', this.onUp);
if (this.dragState) {
this.dragState = null;
try { this.quill.update('user'); } catch (e) {}
}
};
ImageResize.prototype.onDocPointerDown = function (e) {
if (!this.activeImg) return;
if (this.overlay && this.overlay.contains(e.target)) return;
if (e.target === this.activeImg) return;
this.detach();
};
ImageResize.prototype.detach = function () {
if (this.overlay && this.overlay.parentNode) {
this.overlay.parentNode.removeChild(this.overlay);
}
this.overlay = null;
this.activeImg = null;
this.dragState = null;
};
Quill.register('modules/imageResize', ImageResize);
global.QuillImageResize = ImageResize;
})(window);

View File

@@ -0,0 +1,108 @@
/**
* Geteilte Toolbar-Presets für Quill-Editoren (Wiki, Forum).
*
* Jeder Preset ist eine Factory-Funktion, die ein frisches Array
* liefert. Grund: Quill kann das Modules-Objekt während der Init
* mutieren — bei mehreren Editor-Instanzen auf einer Seite (z.B.
* Wiki mit Sprach-Tabs) würden sie sich sonst gegenseitig
* verändern.
*
* Verwendung pro Editor:
*
* container: window.QuillToolbarPresets.full()
*
* Bewusst nicht enthalten: `code` (inline) — nur `code-block`,
* weil Multi-Line-Code die häufige Nutzung ist.
*/
(function (global) {
'use strict';
global.QuillToolbarPresets = {
// Vollständige Toolbar — Goldstandard für Wiki und Forum
full: function () {
return [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ script: 'sub' }, { script: 'super' }],
[{ color: [] }, { background: [] }],
[{ align: [] }],
[
{ list: 'ordered' }, { list: 'bullet' },
{ indent: '-1' }, { indent: '+1' }
],
['link', 'image', 'video'],
['code-block', 'table'],
['clean']
];
},
// Compact-Toolbar — wie full(), aber OHNE Image und Video.
// Genutzt in Modulen, die kein Image-Backend haben (Tasks, Products).
compact: function () {
return [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ script: 'sub' }, { script: 'super' }],
[{ color: [] }, { background: [] }],
[{ align: [] }],
[
{ list: 'ordered' }, { list: 'bullet' },
{ indent: '-1' }, { indent: '+1' }
],
['link'],
['code-block', 'table'],
['clean']
];
},
// Better-Table-Modul-Konfiguration. Identisch über alle Editoren —
// deutsche Operations-Menu-Labels und der Hintergrundfarben-Picker
// für Tabellenzellen.
betterTableConfig: function () {
return {
operationMenu: {
items: {
insertRowUp: { text: 'Zeile oberhalb einfügen' },
insertRowDown: { text: 'Zeile unterhalb einfügen' },
insertColumnLeft: { text: 'Spalte links einfügen' },
insertColumnRight: { text: 'Spalte rechts einfügen' },
mergeCells: { text: 'Zellen zusammenführen' },
unmergeCells: { text: 'Zellen teilen' },
deleteRow: { text: 'Zeile löschen' },
deleteColumn: { text: 'Spalte löschen' },
deleteTable: { text: 'Tabelle löschen' }
},
color: {
colors: [
'#f8f9fa', '#e9ecef', '#dee2e6', '#ced4da',
'#adb5bd', '#6c757d',
'green', 'red', 'yellow', 'blue',
'white', 'black', 'orange'
],
text: 'Hintergrundfarbe'
}
}
};
}
// Vorlage für künftige Erweiterungen — derzeit nicht aktiv:
//
// compact: function () {
// // wie full, aber ohne 'table' — z.B. für Comment-Felder
// var t = this.full();
// var idx = t.findIndex(function (row) { return row.indexOf('table') > -1; });
// if (idx > -1) t[idx] = ['code-block'];
// return t;
// },
//
// minimal: function () {
// return [
// [{ header: [2, 3, false] }],
// ['bold', 'italic', 'underline'],
// [{ list: 'ordered' }, { list: 'bullet' }],
// ['link'],
// ['clean']
// ];
// }
};
})(window);

View File

@@ -260,6 +260,18 @@ if (!defined('TASK_CERT_BOOTSTRAPPED')) {
} }
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
// Endpoint-Requests überspringen start.inc.php (kein session_name-Setup durch den Monolith).
// Cookie-Detection stellt sicher, dass wir dieselbe Session wie der Monolith verwenden
// (Monolith setzt den Session-Namen auf "sid{site_code}", z.B. "sidawo").
if (session_name() === 'PHPSESSID') {
foreach (array_keys($_COOKIE) as $_tc_sn) {
if (strncmp($_tc_sn, 'sid', 3) === 0) {
session_name($_tc_sn);
break;
}
}
unset($_tc_sn);
}
session_start(); session_start();
} }

View File

@@ -747,13 +747,45 @@ function multi_select_permission($navigation_id, $type){
<script type="text/javascript" src="/plugins/jquery/jquery-3.5.1.min.js"></script> <script type="text/javascript" src="/plugins/jquery/jquery-3.5.1.min.js"></script>
<script type="text/javascript" src="/plugins/Semantic-UI-master/semantic.min.js"></script> <script type="text/javascript" src="/plugins/Semantic-UI-master/semantic.min.js"></script>
<link rel="stylesheet" type="text/css" href="/plugins/Semantic-UI-master/bc-semantic.css?time=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/plugins/Semantic-UI-master/bc-semantic.css')?>"> <link rel="stylesheet" type="text/css" href="/plugins/Semantic-UI-master/bc-semantic.css?time=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/plugins/Semantic-UI-master/bc-semantic.css')?>">
<?php
$einrichtLinks = [];
if ($type == "einricht") {
$linkRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT einricht_id, linked_einricht_id FROM organigramm_einricht_link");
while ($lr = mysqli_fetch_assoc($linkRes)) {
$einrichtLinks[strval($lr['einricht_id'])][] = strval($lr['linked_einricht_id']);
}
}
?>
<script> <script>
var $y = jQuery.noConflict(); var $y = jQuery.noConflict();
$y( document ).ready(function() { $y( document ).ready(function() {
$y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>').dropdown('set selected', <?= json_encode($preselect) ?>); var $drop = $y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>');
<?php if ($type == "einricht"): ?>
var einrichtLinks = <?= json_encode($einrichtLinks) ?>;
var busy = false;
$drop.dropdown({
onChange: function(value) {
if (busy) return;
var current = value ? value.split(',').filter(Boolean) : [];
var toAdd = [];
current.forEach(function(id) {
(einrichtLinks[id] || []).forEach(function(lid) {
if (current.indexOf(lid) < 0 && toAdd.indexOf(lid) < 0) toAdd.push(lid);
});
});
if (toAdd.length) {
busy = true;
$drop.dropdown('set selected', current.concat(toAdd));
busy = false;
}
}
});
<?php endif; ?>
$drop.dropdown('set selected', <?= json_encode($preselect) ?>);
}); });
$("<?php echo "#all_".$type;?>").click(function() { $("<?php echo "#all_".$type;?>").click(function() {
$y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>').dropdown('set selected', <?= json_encode($allselect) ?>); $y('.websites.ui.fluid.multiple.dropdown.type_<?= $type ?>').dropdown('set selected', <?= json_encode($allselect) ?>);
}); });
</script> </script>
<? <?

View File

@@ -1216,6 +1216,7 @@ function navigation_fullmenu_rek( $site, $language, $navigation, $parent_id, $le
echo "\n" . $spaces . "<ul class=\"level_$level\">"; echo "\n" . $spaces . "<ul class=\"level_$level\">";
} }
while ($nav = @mysqli_fetch_array($result)) { while ($nav = @mysqli_fetch_array($result)) {
$my_tasks_dot = "";
//Icon herausziehen //Icon herausziehen
$getIconQuery = "SELECT navigation_icon FROM main_navigation WHERE id ='" . $nav["id"] . "'"; $getIconQuery = "SELECT navigation_icon FROM main_navigation WHERE id ='" . $nav["id"] . "'";
$resultIconQuery = mysqli_query($GLOBALS["mysql_con"], $getIconQuery); $resultIconQuery = mysqli_query($GLOBALS["mysql_con"], $getIconQuery);
@@ -1251,6 +1252,37 @@ function navigation_fullmenu_rek( $site, $language, $navigation, $parent_id, $le
if( $nav["code"] == '404' ){ if( $nav["code"] == '404' ){
$display = " style='display:none;' "; $display = " style='display:none;' ";
} }
$hasTaskCertAdminPermission = (int)get_permission_state(
'r-task-cert-admin',
$GLOBALS['main_contact']['master_mandant_id'],
$GLOBALS['main_contact']['id']
);
if ($nav['code'] === 'admin-task-list' && $hasTaskCertAdminPermission === 0) {
$display = " style='display:none;' ";
}
if ($nav["code"] == "my-tasks") {
$query_my_tasks = "
SELECT COUNT(*) AS badge_count
FROM task_assignment ta
INNER JOIN task_definition td
ON td.id = ta.task_id
AND td.active = 1
WHERE ta.main_contact_id = " . (int)$GLOBALS['main_contact']['id'] . "
AND ta.status IN ('overdue', 'open', 'in_progress', 'approval_pending', 'rejected')
";
$result_my_tasks = @mysqli_query($GLOBALS['mysql_con'], $query_my_tasks);
$row_my_tasks = @mysqli_fetch_assoc($result_my_tasks);
$my_tasks_count = (int)$row_my_tasks['badge_count'];
@mysqli_free_result($result_my_tasks);
if ($my_tasks_count > 0) {
$my_tasks_dot = "<span class='hint'>" . $my_tasks_count . "</span>";
}
}
if($nav["code"] == 'home' || $nav["code"] == 'startseite' || $nav["is_home_page"] == 1){ if($nav["code"] == 'home' || $nav["code"] == 'startseite' || $nav["is_home_page"] == 1){
$addr = '/'; $addr = '/';
@@ -1287,7 +1319,7 @@ function navigation_fullmenu_rek( $site, $language, $navigation, $parent_id, $le
echo "</a>"; echo "</a>";
echo "</div>"; echo "</div>";
}else { }else {
echo "<a " . $active . $link . $target .">" . $icon . $nav["menu_name"] . $collection_dot ."</a>"; echo "<a " . $active . $link . $target .">" . $icon . $nav["menu_name"] . $collection_dot . $my_tasks_dot ."</a>";
} }
} }
navigation_fullmenu_rek($site, $language, $navigation, $nav["id"], ($level + 1), $newnavcode, $from_level, $to_level); navigation_fullmenu_rek($site, $language, $navigation, $nav["id"], ($level + 1), $newnavcode, $from_level, $to_level);

View File

@@ -99,7 +99,8 @@ function save_facility() {
$messages[] = '<div class="successbox">' . $translation->get("role_msg_success2") . '</div>'; $messages[] = '<div class="successbox">' . $translation->get("role_msg_success2") . '</div>';
} }
saveMandantLink($input_id); saveMandantLink($input_id);
edit_role($input_id, $messages); saveLinkedEinrichts($input_id);
edit_facility($input_id, $messages);
} else { } else {
header('EDIT_ERROR: 1'); header('EDIT_ERROR: 1');
// $input_role["id"] = $_POST["input_id"]; // $input_role["id"] = $_POST["input_id"];
@@ -110,6 +111,28 @@ function save_facility() {
} }
} }
function saveLinkedEinrichts($einricht_id) {
$einricht_id = (int)$einricht_id;
if ($einricht_id <= 0) return;
// Alle alten Verknüpfungen löschen
mysqli_query($GLOBALS['mysql_con'],
"DELETE FROM organigramm_einricht_link WHERE einricht_id = $einricht_id");
// Neue Verknüpfungen speichern
$raw = trim($_POST['linked_einrichts'] ?? '');
if ($raw === '') return;
foreach (explode(',', $raw) as $linkedId) {
$linkedId = (int)trim($linkedId);
if ($linkedId > 0 && $linkedId !== $einricht_id) {
mysqli_query($GLOBALS['mysql_con'],
"INSERT IGNORE INTO organigramm_einricht_link (einricht_id, linked_einricht_id)
VALUES ($einricht_id, $linkedId)");
}
}
}
function saveMandantLink($role_id){ function saveMandantLink($role_id){
$mandant_id = $_POST['select_mandant']; $mandant_id = $_POST['select_mandant'];
$mandante = explode(',', $mandant_id); $mandante = explode(',', $mandant_id);

View File

@@ -1,48 +1,101 @@
<?php <?php
$formname = "form_facility_card"; $formname = "form_facility_card";
$translation = \DynCom\mysyde\common\classes\Registry::get("translation"); $translation = \DynCom\mysyde\common\classes\Registry::get("translation");
$inputname = "input_id"; $inputname = "input_id";
?>
$currentId = (int)($input_role["id"] ?? 0);
<div id="overlaycrumb">
<?php if ($input_role["id"] == "") { // Alle Einrichtungen außer der aktuellen laden
echo $translation->get("new_facility"); $allEinrichts = [];
} else { $qAll = mysqli_query($GLOBALS['mysql_con'],
echo $translation->get("edit_facility"); "SELECT id, description FROM organigramm_einricht ORDER BY description ASC");
} while ($row = mysqli_fetch_assoc($qAll)) {
?> if ((int)$row['id'] !== $currentId) {
<div id="closeoverlay" onclick="disableOverlay();"></div> $allEinrichts[] = $row;
</div> }
<ul class="toolbar_menu"> }
<?= button("save", $translation->get("save_and_close"), $formname, "loadCard('save', true, '', true)"); ?>
<li> // Bereits verknüpfte Einrichtungen laden
<ul> $preselect = [];
<?php if ($currentId > 0) {
if ($input_role["id"] != "") { $qLinked = mysqli_query($GLOBALS['mysql_con'],
echo button("delete", $translation->get("delete"), $formname, "loadCard('delete', true, '{$translation->get('delete_role_confirm')}', true)", "", FALSE); "SELECT linked_einricht_id FROM organigramm_einricht_link WHERE einricht_id = $currentId");
} while ($row = mysqli_fetch_assoc($qLinked)) {
?> $preselect[] = $row['linked_einricht_id'];
<?= button("reset", $translation->get("restore"), $formname, "?action=edit", "", FALSE); ?> }
</ul> }
</li> ?>
</ul>
<div id="overlaycrumb">
<?php <?php if ($input_role["id"] == "") {
if ($messages !== null) { echo $translation->get("new_facility");
if (count($messages)) { } else {
echo '<div id="overlayMessages">' . join("\r\n", $messages) . '</div>'; echo $translation->get("edit_facility");
} } ?>
} <div id="closeoverlay" onclick="disableOverlay();"></div>
</div>
?> <ul class="toolbar_menu">
<?= button("save", $translation->get("save"), $formname, "loadCard('save', true)"); ?>
<form id="<?= $formname ?>" name="<?= $formname ?>" method="post"> <?= button("save", $translation->get("save_and_close"), $formname, "loadCard('save', true, '', true)"); ?>
<input name="input_id" type="hidden" value="<?= $input_role["id"] ?>"> <li>
<ul>
<table class="cardform" border="0" cellspacing="0" cellpadding="0"> <?php if ($input_role["id"] != ""): ?>
<div> <?= button("delete", $translation->get("delete"), $formname, "loadCard('delete', true, '{$translation->get('delete_role_confirm')}', true)", "", FALSE); ?>
<? input($translation->get("description"), "input_description", "text", $input_role["description"], 100) ?> <?php endif; ?>
</div> <?= button("reset", $translation->get("restore"), $formname, "?action=edit", "", FALSE); ?>
</table> </ul>
</form> </li>
</ul>
<?php if ($messages !== null && count($messages)): ?>
<div id="overlayMessages"><?= join("\r\n", $messages) ?></div>
<?php endif; ?>
<form id="<?= $formname ?>" name="<?= $formname ?>" method="post">
<input name="input_id" type="hidden" value="<?= $input_role["id"] ?>">
<table class="cardform" border="0" cellspacing="0" cellpadding="0">
<div>
<? input($translation->get("description"), "input_description", "text", $input_role["description"], 100) ?>
</div>
<tr>
<div class="label">Verknüpfte Einrichtungen</div>
<div class="einricht-link bc-select-ui ui fluid multiple search selection dropdown" style="clear:both; display:inline-block;">
<input type="hidden" name="linked_einrichts">
<i class="dropdown icon"></i>
<div class="default text">Einrichtungen auswählen…</div>
<div class="menu">
<?php foreach ($allEinrichts as $e): ?>
<div class="item" data-value="<?= (int)$e['id'] ?>">
<?= htmlspecialchars($e['description']) ?>
</div>
<?php endforeach; ?>
</div>
</div>
</tr>
</table>
</form>
<script type="text/javascript" src="/plugins/jquery/jquery-3.5.1.min.js"></script>
<script type="text/javascript" src="/plugins/Semantic-UI-master/semantic.min.js"></script>
<link rel="stylesheet" type="text/css" href="/plugins/Semantic-UI-master/bc-semantic.css?time=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/plugins/Semantic-UI-master/bc-semantic.css') ?>">
<script>
var $y = jQuery.noConflict();
$y(document).ready(function () {
var $drop = $y('.einricht-link.ui.fluid.multiple.dropdown');
var $hidden = $y('input[name="linked_einrichts"]');
$drop.dropdown({
onChange: function (value) {
$hidden.val(value);
}
});
var preselect = <?= json_encode($preselect) ?>.map(String);
if (preselect.length) {
$drop.dropdown('set selected', preselect);
$hidden.val(preselect.join(','));
}
});
</script>

View File

@@ -1,3 +0,0 @@
<?php
// Merged into mysyde/admin/text_constants.inc.php
require_once __DIR__ . '/mysyde/admin/text_constants.inc.php';