Compare commits
4 Commits
55e6d4c583
...
0bf47247ff
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bf47247ff | |||
| 54d9194d22 | |||
| 8ff44a407e | |||
| ee966de174 |
11
layout/frontend/b2c/dist/css/global.css
vendored
11
layout/frontend/b2c/dist/css/global.css
vendored
@@ -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;
|
||||||
|
|||||||
@@ -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>'
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1814,7 +1814,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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
457
module/knowledgecenter_update/Script/migrate_old_wiki.php
Normal file
457
module/knowledgecenter_update/Script/migrate_old_wiki.php
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
<?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 verweist auf filegallery_header
|
||||||
|
$sql = "SELECT mcl.main_sitepart_header_id
|
||||||
|
FROM main_collection_link mcl
|
||||||
|
WHERE mcl.main_collection_id = $cid AND mcl.main_sitepart_id = 5";
|
||||||
|
$res = mysqli_query($GLOBALS['mysql_con'], $sql);
|
||||||
|
$files = [];
|
||||||
|
while ($row = mysqli_fetch_assoc($res)) {
|
||||||
|
$hid = (int)$row['main_sitepart_header_id'];
|
||||||
|
if ($hid === 0) continue;
|
||||||
|
$fres = mysqli_query($GLOBALS['mysql_con'],
|
||||||
|
"SELECT filename, extension, description FROM filegallery_line WHERE header_id = $hid ORDER BY sorting");
|
||||||
|
while ($frow = mysqli_fetch_assoc($fres)) {
|
||||||
|
if (trim($frow['filename']) !== '') {
|
||||||
|
$files[] = $frow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extract_pdf_text(string $filePath): string {
|
||||||
|
if (!file_exists($filePath) || filesize($filePath) === 0) return '';
|
||||||
|
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 {
|
||||||
|
// Fließtext: HTML vollständig entfernen, keine Zeichenbegrenzung
|
||||||
|
$plainText = html_entity_decode(strip_tags($htmlContent), ENT_HTML5 | ENT_QUOTES, 'UTF-8');
|
||||||
|
$plainText = trim(preg_replace('/\s+/', ' ', $plainText));
|
||||||
|
|
||||||
|
$parts = [];
|
||||||
|
if (strlen($plainText) > 30) {
|
||||||
|
$parts[] = $plainText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDFs: Text extrahieren, auf 1500 Zeichen begrenzen (bei sehr langen PDFs)
|
||||||
|
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, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($parts)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kompakter Prompt: Titel + vollständiger Inhalt
|
||||||
|
$prompt = "Fasse diesen AWO-Wiki-Beitrag in 2-3 deutschen Sätzen zusammen. Nur Zusammenfassung, kein Intro.\n"
|
||||||
|
. "Titel: $title\n\n"
|
||||||
|
. implode("\n\n", $parts);
|
||||||
|
|
||||||
|
logmsg(" PROMPT (" . strlen($prompt) . " Zeichen):\n" . $prompt . "\n--- PROMPT END ---", 'PROMPT');
|
||||||
|
|
||||||
|
$payload = json_encode([
|
||||||
|
'contents' => [['parts' => [['text' => $prompt]]]],
|
||||||
|
'generationConfig' => [
|
||||||
|
'maxOutputTokens' => 300,
|
||||||
|
'temperature' => 0.2,
|
||||||
|
'thinkingConfig' => ['thinkingBudget' => 0], // kein Thinking = alle Token für Antwort
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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.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);
|
||||||
|
$summary = generate_summary($sumRow['content'], $files, $userdataDir, $apiKey, $title) ?? '';
|
||||||
|
|
||||||
|
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']++;
|
||||||
|
}
|
||||||
|
usleep(200000);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)'));
|
||||||
|
usleep(200000); // 200ms Pause für API rate limiting
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
@@ -47,7 +47,7 @@ $redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=Lis
|
|||||||
<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 if (get_permission_state('r-wiki', $GLOBALS['main_contact']['master_mandant_id'], $GLOBALS['main_contact']['id']) == 1) { ?>
|
||||||
<a href="?action=NewPost&InitialCategory=<?= $_GET["detail"] ?>"
|
<a href="?action=NewPost&InitialCategory=<?= $_GET["detail"] ?>"
|
||||||
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>
|
||||||
@@ -59,8 +59,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:
|
||||||
@@ -119,6 +119,7 @@ $redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=Lis
|
|||||||
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
|
||||||
@@ -161,6 +162,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>";
|
||||||
}
|
}
|
||||||
@@ -270,6 +274,7 @@ $redirectURL = isset($_GET['redirectURL']) ? $_GET['redirectURL'] : '?action=Lis
|
|||||||
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
|
||||||
@@ -305,11 +310,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>";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,6 +211,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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,59 @@
|
|||||||
.ql-img-resize-overlay {
|
/* Drag-Handle Overlay für Quill Image Resize */
|
||||||
position: absolute;
|
|
||||||
border: 1px dashed #3C7DD9;
|
.ql-image-overlay {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
border: 1px dashed #2684ff;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 100;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ql-img-resize-handle {
|
.ql-image-handle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -5px;
|
width: 12px;
|
||||||
right: -5px;
|
height: 12px;
|
||||||
width: 10px;
|
background: #ffffff;
|
||||||
height: 10px;
|
border: 2px solid #2684ff;
|
||||||
background: #3C7DD9;
|
|
||||||
border: 1px solid #fff;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
cursor: nwse-resize;
|
pointer-events: auto;
|
||||||
pointer-events: all;
|
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. */
|
||||||
|
|||||||
@@ -1,96 +1,363 @@
|
|||||||
(function () {
|
/**
|
||||||
|
* 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';
|
'use strict';
|
||||||
|
|
||||||
if (typeof Quill === 'undefined') return;
|
if (typeof Quill === 'undefined') {
|
||||||
|
console.error('[quill_resize] Quill ist nicht geladen.');
|
||||||
function ImageResizeModule(quill, options) {
|
return;
|
||||||
this.quill = quill;
|
|
||||||
this.overlay = null;
|
|
||||||
this.activeImg = null;
|
|
||||||
this._startX = 0;
|
|
||||||
this._startW = 0;
|
|
||||||
|
|
||||||
this._onImgClick = this._onImgClick.bind(this);
|
|
||||||
this._onDocClick = this._onDocClick.bind(this);
|
|
||||||
this._onMouseMove = this._onMouseMove.bind(this);
|
|
||||||
this._onMouseUp = this._onMouseUp.bind(this);
|
|
||||||
|
|
||||||
quill.root.addEventListener('click', this._onImgClick);
|
|
||||||
document.addEventListener('click', this._onDocClick);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageResizeModule.prototype._onImgClick = function (e) {
|
// ResizableImage-Blot: Quill 2.0.0-dev.3' nativer formats/image strippt
|
||||||
if (e.target && e.target.tagName === 'IMG') {
|
// beim Re-Parse alle Attribute außer src. Dieser Blot whitelisted alt,
|
||||||
e.stopPropagation();
|
// width, height, damit die per Drag gesetzte Größe den Roundtrip überlebt.
|
||||||
this._select(e.target);
|
//
|
||||||
|
// 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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ImageResizeModule.prototype._onDocClick = function (e) {
|
ImageResize.prototype.attachTo = function (img) {
|
||||||
if (this.overlay && e.target !== this.activeImg && !this.overlay.contains(e.target)) {
|
if (this.activeImg === img && this.overlay) return;
|
||||||
this._deselect();
|
this.detach();
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ImageResizeModule.prototype._select = function (img) {
|
|
||||||
this._deselect();
|
|
||||||
this.activeImg = img;
|
this.activeImg = img;
|
||||||
|
this.overlay = this.buildOverlay();
|
||||||
|
document.body.appendChild(this.overlay);
|
||||||
|
this.reposition();
|
||||||
|
};
|
||||||
|
|
||||||
var overlay = document.createElement('div');
|
ImageResize.prototype.buildOverlay = function () {
|
||||||
overlay.className = 'ql-img-resize-overlay';
|
|
||||||
|
|
||||||
var handle = document.createElement('div');
|
|
||||||
handle.className = 'ql-img-resize-handle';
|
|
||||||
overlay.appendChild(handle);
|
|
||||||
|
|
||||||
this.quill.root.style.position = 'relative';
|
|
||||||
this.quill.root.appendChild(overlay);
|
|
||||||
this.overlay = overlay;
|
|
||||||
|
|
||||||
this._reposition();
|
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
handle.addEventListener('mousedown', function (e) {
|
var box = document.createElement('div');
|
||||||
e.preventDefault();
|
box.className = 'ql-image-overlay';
|
||||||
self._startX = e.clientX;
|
|
||||||
self._startW = self.activeImg.offsetWidth || self.activeImg.getBoundingClientRect().width;
|
['nw', 'ne', 'sw', 'se'].forEach(function (corner) {
|
||||||
document.addEventListener('mousemove', self._onMouseMove);
|
var h = document.createElement('div');
|
||||||
document.addEventListener('mouseup', self._onMouseUp);
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
ImageResizeModule.prototype._reposition = function () {
|
ImageResize.prototype.reposition = function () {
|
||||||
if (!this.overlay || !this.activeImg) return;
|
if (!this.activeImg || !this.overlay) return;
|
||||||
var r = this.activeImg.getBoundingClientRect();
|
var r = this.activeImg.getBoundingClientRect();
|
||||||
var er = this.quill.root.getBoundingClientRect();
|
var s = this.overlay.style;
|
||||||
this.overlay.style.left = (r.left - er.left + this.quill.root.scrollLeft) + 'px';
|
s.position = 'fixed';
|
||||||
this.overlay.style.top = (r.top - er.top + this.quill.root.scrollTop) + 'px';
|
s.left = r.left + 'px';
|
||||||
this.overlay.style.width = r.width + 'px';
|
s.top = r.top + 'px';
|
||||||
this.overlay.style.height = r.height + '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';
|
||||||
};
|
};
|
||||||
|
|
||||||
ImageResizeModule.prototype._onMouseMove = function (e) {
|
ImageResize.prototype.onReposition = function () {
|
||||||
|
this.reposition();
|
||||||
|
};
|
||||||
|
|
||||||
|
ImageResize.prototype.onHandleDown = function (e, corner) {
|
||||||
if (!this.activeImg) return;
|
if (!this.activeImg) return;
|
||||||
var dx = e.clientX - this._startX;
|
e.preventDefault();
|
||||||
var newW = Math.max(40, this._startW + dx);
|
e.stopPropagation();
|
||||||
this.activeImg.style.width = newW + 'px';
|
|
||||||
this.activeImg.setAttribute('width', newW);
|
var p = (e.touches && e.touches[0]) ? e.touches[0] : e;
|
||||||
this._reposition();
|
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
ImageResizeModule.prototype._onMouseUp = function () {
|
ImageResize.prototype.onMove = function (e) {
|
||||||
document.removeEventListener('mousemove', this._onMouseMove);
|
if (!this.dragState || !this.activeImg) return;
|
||||||
document.removeEventListener('mouseup', this._onMouseUp);
|
if (e.cancelable) e.preventDefault();
|
||||||
if (this.quill) this.quill.update();
|
|
||||||
|
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();
|
||||||
};
|
};
|
||||||
|
|
||||||
ImageResizeModule.prototype._deselect = function () {
|
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) {
|
if (this.overlay && this.overlay.parentNode) {
|
||||||
this.overlay.parentNode.removeChild(this.overlay);
|
this.overlay.parentNode.removeChild(this.overlay);
|
||||||
}
|
}
|
||||||
this.overlay = null;
|
this.overlay = null;
|
||||||
this.activeImg = null;
|
this.activeImg = null;
|
||||||
|
this.dragState = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
Quill.register('modules/imageResize', ImageResizeModule);
|
Quill.register('modules/imageResize', ImageResize);
|
||||||
})();
|
global.QuillImageResize = ImageResize;
|
||||||
|
})(window);
|
||||||
|
|||||||
@@ -1,46 +1,108 @@
|
|||||||
window.QuillToolbarPresets = {
|
/**
|
||||||
|
* 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';
|
||||||
|
|
||||||
full: function () {
|
global.QuillToolbarPresets = {
|
||||||
return [
|
// Vollständige Toolbar — Goldstandard für Wiki und Forum
|
||||||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
full: function () {
|
||||||
['bold', 'italic', 'underline', 'strike'],
|
return [
|
||||||
[{ color: [] }, { background: [] }],
|
[{ header: [1, 2, 3, false] }],
|
||||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
['bold', 'italic', 'underline', 'strike'],
|
||||||
[{ indent: '-1' }, { indent: '+1' }],
|
[{ script: 'sub' }, { script: 'super' }],
|
||||||
[{ align: [] }],
|
[{ color: [] }, { background: [] }],
|
||||||
['blockquote', 'code-block'],
|
[{ align: [] }],
|
||||||
['link', 'image'],
|
[
|
||||||
['table'],
|
{ list: 'ordered' }, { list: 'bullet' },
|
||||||
['clean']
|
{ indent: '-1' }, { indent: '+1' }
|
||||||
];
|
],
|
||||||
},
|
['link', 'image', 'video'],
|
||||||
|
['code-block', 'table'],
|
||||||
|
['clean']
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
compact: function () {
|
// Compact-Toolbar — wie full(), aber OHNE Image und Video.
|
||||||
return [
|
// Genutzt in Modulen, die kein Image-Backend haben (Tasks, Products).
|
||||||
[{ header: [1, 2, 3, false] }],
|
compact: function () {
|
||||||
['bold', 'italic', 'underline'],
|
return [
|
||||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
[{ header: [1, 2, 3, false] }],
|
||||||
['link', 'image'],
|
['bold', 'italic', 'underline', 'strike'],
|
||||||
['table'],
|
[{ script: 'sub' }, { script: 'super' }],
|
||||||
['clean']
|
[{ color: [] }, { background: [] }],
|
||||||
];
|
[{ align: [] }],
|
||||||
},
|
[
|
||||||
|
{ list: 'ordered' }, { list: 'bullet' },
|
||||||
|
{ indent: '-1' }, { indent: '+1' }
|
||||||
|
],
|
||||||
|
['link'],
|
||||||
|
['code-block', 'table'],
|
||||||
|
['clean']
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
betterTableConfig: function () {
|
// Better-Table-Modul-Konfiguration. Identisch über alle Editoren —
|
||||||
return {
|
// deutsche Operations-Menu-Labels und der Hintergrundfarben-Picker
|
||||||
operationMenu: {
|
// für Tabellenzellen.
|
||||||
items: {
|
betterTableConfig: function () {
|
||||||
insertColumnRight: { text: 'Spalte rechts' },
|
return {
|
||||||
insertColumnLeft: { text: 'Spalte links' },
|
operationMenu: {
|
||||||
insertRowUp: { text: 'Zeile oben' },
|
items: {
|
||||||
insertRowDown: { text: 'Zeile unten' },
|
insertRowUp: { text: 'Zeile oberhalb einfügen' },
|
||||||
mergeCells: { text: 'Zellen verbinden' },
|
insertRowDown: { text: 'Zeile unterhalb einfügen' },
|
||||||
unmergeCells: { text: 'Zellen trennen' },
|
insertColumnLeft: { text: 'Spalte links einfügen' },
|
||||||
deleteColumn: { text: 'Spalte löschen' },
|
insertColumnRight: { text: 'Spalte rechts einfügen' },
|
||||||
deleteRow: { text: 'Zeile löschen' },
|
mergeCells: { text: 'Zellen zusammenführen' },
|
||||||
deleteTable: { text: 'Tabelle löschen' }
|
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);
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -1252,6 +1253,37 @@ function navigation_fullmenu_rek( $site, $language, $navigation, $parent_id, $le
|
|||||||
$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);
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Merged into mysyde/admin/text_constants.inc.php
|
|
||||||
require_once __DIR__ . '/mysyde/admin/text_constants.inc.php';
|
|
||||||
Reference in New Issue
Block a user