4219 lines
129 KiB
PHP
4219 lines
129 KiB
PHP
<?php
|
|
require '../config.php';
|
|
require '../auth.php';
|
|
|
|
if (!isset($_SESSION['username'])) {
|
|
http_response_code(401);
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode(['ok' => false, 'error' => 'Nicht eingeloggt.']);
|
|
exit;
|
|
}
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
define('VISU_ASSET_UPLOAD_DIR', dirname(__DIR__) . '/uploads/visu_assets');
|
|
define('VISU_ASSET_UPLOAD_URL', '/uploads/visu_assets');
|
|
define('VISU_BG_UPLOAD_DIR', dirname(__DIR__) . '/uploads/visu_backgrounds');
|
|
define('VISU_BG_UPLOAD_URL', '/uploads/visu_backgrounds');
|
|
define('VISU_INFLUX_URL', 'http://10.99.99.6:8086');
|
|
define('VISU_EXPORT_STORAGE_ROOT', rtrim((string)(getenv('VISU_EXPORT_ROOT') ?: '/var/portal_storage'), '/'));
|
|
|
|
function visuJson(array $data, int $status = 200): void
|
|
{
|
|
http_response_code($status);
|
|
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
function visuCleanString($value): ?string
|
|
{
|
|
$value = trim((string)$value);
|
|
return $value === '' ? null : $value;
|
|
}
|
|
|
|
function visuSlug(string $value): string
|
|
{
|
|
$value = trim(mb_strtolower($value));
|
|
$value = preg_replace('/[^a-z0-9äöüß]+/iu', '-', $value);
|
|
$value = trim((string)$value, '-');
|
|
return $value !== '' ? mb_substr($value, 0, 180) : 'visu';
|
|
}
|
|
|
|
function visuUserGroups(PDO $pdo, string $username, array $sessionGroups = []): array
|
|
{
|
|
$groups = is_array($sessionGroups) ? $sessionGroups : [];
|
|
|
|
if (!empty($groups)) {
|
|
return array_values(array_unique(array_filter(array_map('trim', $groups), fn($x) => $x !== '')));
|
|
}
|
|
|
|
if ($username === '') {
|
|
return [];
|
|
}
|
|
|
|
$stmt = $pdo->prepare("SELECT `groups` FROM users WHERE username = ? LIMIT 1");
|
|
$stmt->execute([$username]);
|
|
$raw = (string)($stmt->fetchColumn() ?? '');
|
|
|
|
if ($raw === '') {
|
|
return [];
|
|
}
|
|
|
|
$parts = array_map('trim', explode('|', $raw));
|
|
return array_values(array_unique(array_filter($parts, fn($x) => $x !== '')));
|
|
}
|
|
|
|
function visuVisibilitySql(int $userId, bool $canViewAll, string $projectExpr, array &$params, string $prefix): string
|
|
{
|
|
if ($canViewAll) {
|
|
return '1=1';
|
|
}
|
|
|
|
$params[":{$prefix}_user_id"] = $userId;
|
|
$params[":{$prefix}_role_user_id"] = $userId;
|
|
|
|
return "(
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM project_assignments pa
|
|
WHERE pa.project_id = {$projectExpr}
|
|
AND pa.user_id = :{$prefix}_user_id
|
|
)
|
|
OR EXISTS (
|
|
SELECT 1
|
|
FROM project_groups pg
|
|
JOIN roles r ON r.key_name = pg.group_name
|
|
JOIN user_roles ur ON ur.role_id = r.id
|
|
WHERE pg.project_id = {$projectExpr}
|
|
AND ur.user_id = :{$prefix}_role_user_id
|
|
)
|
|
)";
|
|
}
|
|
|
|
function visuProjectVisible(PDO $pdo, int $projectId, int $userId, bool $canViewAll): bool
|
|
{
|
|
if ($projectId <= 0) {
|
|
return false;
|
|
}
|
|
|
|
$params = [':project_id' => $projectId];
|
|
$visSql = visuVisibilitySql($userId, $canViewAll, 'p.id', $params, 'project_visible');
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT 1
|
|
FROM projects p
|
|
WHERE p.id = :project_id
|
|
AND {$visSql}
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute($params);
|
|
|
|
return (bool)$stmt->fetchColumn();
|
|
}
|
|
|
|
function visuPageVisible(PDO $pdo, int $pageId, int $userId, bool $canViewAll): ?array
|
|
{
|
|
if ($pageId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$params = [':page_id' => $pageId];
|
|
$visSql = visuVisibilitySql($userId, $canViewAll, 'vp.project_id', $params, 'page_visible');
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
vp.*,
|
|
p.project_number,
|
|
p.year,
|
|
p.name AS project_name,
|
|
pc.company_name AS customer_name
|
|
FROM visu_pages vp
|
|
INNER JOIN projects p ON p.id = vp.project_id
|
|
LEFT JOIN project_customers pc ON pc.id = p.customer_id
|
|
WHERE vp.id = :page_id
|
|
AND vp.is_active = 1
|
|
AND {$visSql}
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute($params);
|
|
|
|
$page = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
return $page ?: null;
|
|
}
|
|
|
|
function visuEnsureDir(string $dir): void
|
|
{
|
|
if (!is_dir($dir)) {
|
|
@mkdir($dir, 0775, true);
|
|
}
|
|
|
|
if (!is_dir($dir) || !is_writable($dir)) {
|
|
visuJson([
|
|
'ok' => false,
|
|
'error' => 'Upload-Ordner ist nicht beschreibbar: ' . $dir,
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
function visuSaveUploadedImage(string $fieldName, string $dir, string $urlBase): ?string
|
|
{
|
|
if (empty($_FILES[$fieldName]) || !is_array($_FILES[$fieldName])) {
|
|
return null;
|
|
}
|
|
|
|
$file = $_FILES[$fieldName];
|
|
|
|
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
|
|
return null;
|
|
}
|
|
|
|
if (($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
|
|
visuJson(['ok' => false, 'error' => 'Uploadfehler bei ' . $fieldName], 400);
|
|
}
|
|
|
|
$tmp = (string)($file['tmp_name'] ?? '');
|
|
$name = (string)($file['name'] ?? '');
|
|
|
|
if (!is_uploaded_file($tmp)) {
|
|
visuJson(['ok' => false, 'error' => 'Ungültiger Upload.'], 400);
|
|
}
|
|
|
|
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
|
$allowed = ['png', 'jpg', 'jpeg', 'webp', 'svg'];
|
|
|
|
if (!in_array($ext, $allowed, true)) {
|
|
visuJson(['ok' => false, 'error' => 'Nur PNG, JPG, JPEG, WEBP oder SVG erlaubt.'], 400);
|
|
}
|
|
|
|
visuEnsureDir($dir);
|
|
|
|
$stored = date('Ymd_His') . '_' . bin2hex(random_bytes(10)) . '.' . $ext;
|
|
$target = rtrim($dir, '/') . '/' . $stored;
|
|
|
|
if (!move_uploaded_file($tmp, $target)) {
|
|
visuJson(['ok' => false, 'error' => 'Datei konnte nicht gespeichert werden.'], 500);
|
|
}
|
|
|
|
@chmod($target, 0664);
|
|
|
|
return rtrim($urlBase, '/') . '/' . $stored;
|
|
}
|
|
|
|
function visuSaveUploadedImageFile(array $file, string $dir, string $urlBase): ?array
|
|
{
|
|
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
|
|
return null;
|
|
}
|
|
|
|
if (($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
|
|
visuJson(['ok' => false, 'error' => 'Uploadfehler bei Bild.'], 400);
|
|
}
|
|
|
|
$tmp = (string)($file['tmp_name'] ?? '');
|
|
$name = (string)($file['name'] ?? '');
|
|
|
|
if (!is_uploaded_file($tmp)) {
|
|
visuJson(['ok' => false, 'error' => 'Ungueltiger Upload.'], 400);
|
|
}
|
|
|
|
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
|
$allowed = ['png', 'jpg', 'jpeg', 'webp', 'svg'];
|
|
|
|
if (!in_array($ext, $allowed, true)) {
|
|
visuJson(['ok' => false, 'error' => 'Nur PNG, JPG, JPEG, WEBP oder SVG erlaubt.'], 400);
|
|
}
|
|
|
|
visuEnsureDir($dir);
|
|
|
|
$stored = date('Ymd_His') . '_' . bin2hex(random_bytes(10)) . '.' . $ext;
|
|
$target = rtrim($dir, '/') . '/' . $stored;
|
|
|
|
if (!move_uploaded_file($tmp, $target)) {
|
|
visuJson(['ok' => false, 'error' => 'Datei konnte nicht gespeichert werden.'], 500);
|
|
}
|
|
|
|
@chmod($target, 0664);
|
|
|
|
return [
|
|
'url' => rtrim($urlBase, '/') . '/' . $stored,
|
|
'original_name' => $name,
|
|
'mime_type' => (string)($file['type'] ?? ''),
|
|
'file_size' => (int)($file['size'] ?? 0),
|
|
];
|
|
}
|
|
|
|
function visuImageLibraryRegister(PDO $pdo, string $url, ?string $originalName, ?string $mimeType, ?int $fileSize, ?int $projectId, int $userId): void
|
|
{
|
|
$url = trim($url);
|
|
if ($url === '') {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO visu_image_library
|
|
(project_id, file_url, original_name, mime_type, file_size, created_by, is_active)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, 1)
|
|
ON DUPLICATE KEY UPDATE
|
|
project_id = COALESCE(VALUES(project_id), project_id),
|
|
original_name = COALESCE(VALUES(original_name), original_name),
|
|
mime_type = COALESCE(VALUES(mime_type), mime_type),
|
|
file_size = COALESCE(VALUES(file_size), file_size),
|
|
is_active = 1
|
|
");
|
|
$stmt->execute([$projectId, $url, $originalName, $mimeType, $fileSize, $userId]);
|
|
} catch (Throwable $e) {
|
|
// Die Bilderbibliothek ist optional bis die SQL-Tabelle angelegt wurde.
|
|
}
|
|
}
|
|
|
|
function visuImageLibraryAllowed(PDO $pdo, string $url, int $projectId, int $userId, bool $canViewAll): bool
|
|
{
|
|
$url = trim($url);
|
|
if ($url === '' || !str_starts_with($url, rtrim(VISU_ASSET_UPLOAD_URL, '/') . '/')) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$stmt = $pdo->prepare("
|
|
SELECT project_id
|
|
FROM visu_image_library
|
|
WHERE file_url = ?
|
|
AND is_active = 1
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([$url]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$row) {
|
|
return false;
|
|
}
|
|
|
|
$imageProjectId = (int)($row['project_id'] ?? 0);
|
|
|
|
if ($imageProjectId <= 0) {
|
|
return true;
|
|
}
|
|
|
|
if ($projectId > 0 && $imageProjectId === $projectId) {
|
|
return true;
|
|
}
|
|
|
|
return visuProjectVisible($pdo, $imageProjectId, $userId, $canViewAll);
|
|
} catch (Throwable $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function visuDeleteAssetImageIfUnused(PDO $pdo, ?string $url, int $ignoreStateId = 0): void
|
|
{
|
|
$url = trim((string)$url);
|
|
if ($url === '') {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM visu_image_library WHERE file_url = ? AND is_active = 1");
|
|
$stmt->execute([$url]);
|
|
if ((int)$stmt->fetchColumn() > 0) {
|
|
return;
|
|
}
|
|
} catch (Throwable $e) {
|
|
// Ohne Bibliothekstabelle weiter wie bisher.
|
|
}
|
|
|
|
try {
|
|
if ($ignoreStateId > 0) {
|
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM visu_asset_states WHERE image_file = ? AND id <> ?");
|
|
$stmt->execute([$url, $ignoreStateId]);
|
|
} else {
|
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM visu_asset_states WHERE image_file = ?");
|
|
$stmt->execute([$url]);
|
|
}
|
|
|
|
if ((int)$stmt->fetchColumn() > 0) {
|
|
return;
|
|
}
|
|
} catch (Throwable $e) {
|
|
}
|
|
|
|
visuDeleteUploadedFile($url, VISU_ASSET_UPLOAD_URL, VISU_ASSET_UPLOAD_DIR);
|
|
}
|
|
|
|
function visuDeleteUploadedFile(?string $url, string $urlBase, string $dir): void
|
|
{
|
|
$url = trim((string)$url);
|
|
if ($url === '') {
|
|
return;
|
|
}
|
|
|
|
$prefix = rtrim($urlBase, '/') . '/';
|
|
|
|
if (!str_starts_with($url, $prefix)) {
|
|
return;
|
|
}
|
|
|
|
$file = basename($url);
|
|
$path = rtrim($dir, '/') . '/' . $file;
|
|
|
|
$base = realpath($dir);
|
|
$real = realpath($path);
|
|
|
|
if ($base && $real && strncmp($real, $base, strlen($base)) === 0 && is_file($real)) {
|
|
@unlink($real);
|
|
}
|
|
}
|
|
|
|
function visuCallMainApi(string $fn, string $externalId, array $extra = []): array
|
|
{
|
|
global $mainApiKey;
|
|
|
|
$apiKey = (string)($mainApiKey ?? '');
|
|
|
|
if ($apiKey === '') {
|
|
return ['ok' => false, 'error' => 'mainApiKey fehlt in config.php'];
|
|
}
|
|
|
|
$data = array_merge([
|
|
'api_key' => $apiKey,
|
|
'function' => $fn,
|
|
'id' => $externalId,
|
|
], $extra);
|
|
|
|
$ch = curl_init('https://main.api.se-inno.de');
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 12);
|
|
|
|
$resp = curl_exec($ch);
|
|
$err = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($err) {
|
|
return ['ok' => false, 'error' => $err];
|
|
}
|
|
|
|
$decoded = json_decode((string)$resp, true);
|
|
return is_array($decoded) ? $decoded : ['ok' => false, 'error' => 'upstream bad json', 'raw' => (string)$resp];
|
|
}
|
|
|
|
function visuPointLabel(array $p, int $ispNumber, string $type): string
|
|
{
|
|
$group = trim((string)($p['group'] ?? ''));
|
|
$label = trim((string)($p['label'] ?? ''));
|
|
$key = trim((string)($p['key'] ?? ''));
|
|
$id = (int)($p['id'] ?? 0);
|
|
|
|
if ($label === '' || in_array(mb_strtolower($label), ['value', 'wert'], true)) {
|
|
$label = $key !== '' ? $key : ('Variable ' . $id);
|
|
}
|
|
|
|
return trim(implode(' · ', array_filter([
|
|
'ISP ' . $ispNumber,
|
|
strtoupper($type),
|
|
$group,
|
|
$label,
|
|
])));
|
|
}
|
|
|
|
function visuPointSourceKey(array $point): string
|
|
{
|
|
return implode('|', [
|
|
(string)($point['type'] ?? ''),
|
|
(string)($point['external_id'] ?? ''),
|
|
(string)($point['variable_id'] ?? ''),
|
|
(string)($point['point_key'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
function visuPointHasDisplayValue(array $point): bool
|
|
{
|
|
foreach (['value', 'value_formatted'] as $key) {
|
|
if (array_key_exists($key, $point) && trim((string)$point[$key]) !== '') {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function visuNormalizeScalarValue($value): ?string
|
|
{
|
|
if (is_bool($value)) {
|
|
return $value ? '1' : '0';
|
|
}
|
|
|
|
if (is_int($value) || is_float($value) || is_string($value)) {
|
|
return trim((string)$value);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function visuPickFirstScalar(array $payload, array $keys): ?string
|
|
{
|
|
foreach ($keys as $key) {
|
|
if (array_key_exists($key, $payload)) {
|
|
$value = visuNormalizeScalarValue($payload[$key]);
|
|
|
|
if ($value !== null && $value !== '') {
|
|
return $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($payload as $value) {
|
|
if (is_array($value)) {
|
|
$nested = visuPickFirstScalar($value, $keys);
|
|
|
|
if ($nested !== null && $nested !== '') {
|
|
return $nested;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function visuObjectIdFromValue($value): int
|
|
{
|
|
$scalar = visuNormalizeScalarValue($value);
|
|
|
|
if ($scalar === null || $scalar === '') {
|
|
return 0;
|
|
}
|
|
|
|
$clean = str_replace(['.', ',', ' '], '', $scalar);
|
|
|
|
if (!preg_match('/^\d+$/', $clean)) {
|
|
return 0;
|
|
}
|
|
|
|
$id = (int)$clean;
|
|
return $id > 1000 ? $id : 0;
|
|
}
|
|
|
|
function visuNormalizeAlarmValue(?string $value, bool $allowObjectIds = false): ?string
|
|
{
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
|
|
$clean = trim($value);
|
|
|
|
if ($clean === '') {
|
|
return null;
|
|
}
|
|
|
|
$lower = mb_strtolower($clean);
|
|
|
|
if (in_array($lower, ['ok', 'aus', 'off', 'false', 'nein', 'no', 'normal'], true)) {
|
|
return '0';
|
|
}
|
|
|
|
if (in_array($lower, ['störung', 'stoerung', 'alarm', 'ein', 'on', 'true', 'ja', 'yes', 'active', 'aktiv'], true)) {
|
|
return '1';
|
|
}
|
|
|
|
if (preg_match('/-?\d+(?:[.,]\d+)?/', $clean, $match)) {
|
|
$numeric = str_replace(',', '.', $match[0]);
|
|
|
|
if (!$allowObjectIds && abs((float)$numeric) > 1) {
|
|
return null;
|
|
}
|
|
|
|
return $numeric;
|
|
}
|
|
|
|
return $clean;
|
|
}
|
|
|
|
function visuAlarmValueFromResponse(array $response): ?string
|
|
{
|
|
$value = visuPickFirstScalar($response, ['value_raw', 'raw_value', 'raw', 'value', 'Value', 'val', 'result', 'formatted', 'state']);
|
|
return visuNormalizeAlarmValue($value);
|
|
}
|
|
|
|
function visuAlarmValueFromPointPayload(array $point): ?string
|
|
{
|
|
$value = visuPickFirstScalar($point, ['value_raw', 'raw_value', 'raw', 'value', 'Value', 'val', 'result', 'value_formatted', 'formatted', 'state']);
|
|
return visuNormalizeAlarmValue($value);
|
|
}
|
|
|
|
function visuMatchingAlarmPoint(array $points, array $needle): ?array
|
|
{
|
|
$needleKey = (string)($needle['point_key'] ?? '');
|
|
$needleGroup = (string)($needle['group'] ?? '');
|
|
$needleLabel = (string)($needle['raw_label'] ?? '');
|
|
|
|
foreach ($points as $point) {
|
|
if (!is_array($point)) {
|
|
continue;
|
|
}
|
|
|
|
if ($needleKey !== '' && (string)($point['key'] ?? '') === $needleKey) {
|
|
return $point;
|
|
}
|
|
|
|
if (
|
|
$needleGroup !== ''
|
|
&& $needleLabel !== ''
|
|
&& (string)($point['group'] ?? '') === $needleGroup
|
|
&& (string)($point['label'] ?? '') === $needleLabel
|
|
) {
|
|
return $point;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function visuHydrateAlarmPointValue(array $point): array
|
|
{
|
|
$variableId = (int)($point['variable_id'] ?? 0);
|
|
$alarmMasterId =
|
|
visuObjectIdFromValue($point['value'] ?? null)
|
|
?: visuObjectIdFromValue($point['value_formatted'] ?? null)
|
|
?: visuObjectIdFromValue($point['alarm_master_id'] ?? null);
|
|
|
|
if ($variableId <= 0 && $alarmMasterId <= 0) {
|
|
return $point;
|
|
}
|
|
|
|
$rawValue = null;
|
|
$sourceResponse = null;
|
|
|
|
if ($alarmMasterId > 0) {
|
|
$alarmList = visuCallMainApi('getalarms', (string)$alarmMasterId);
|
|
$alarmPoints = $alarmList['points'] ?? $alarmList['alarms'] ?? [];
|
|
|
|
if (is_array($alarmPoints)) {
|
|
$match = visuMatchingAlarmPoint($alarmPoints, $point);
|
|
|
|
if (is_array($match)) {
|
|
$candidate = visuAlarmValueFromPointPayload($match);
|
|
|
|
if ($candidate !== null && $candidate !== '') {
|
|
$rawValue = $candidate;
|
|
$sourceResponse = $match;
|
|
$point['alarm_value_id'] = (int)($match['id'] ?? 0) ?: null;
|
|
$point['alarm_master_id'] = $alarmMasterId;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($rawValue === null || $rawValue === '') {
|
|
$valueResponse = visuCallMainApi('getvalue', (string)$alarmMasterId);
|
|
$candidate = is_array($valueResponse) ? visuAlarmValueFromResponse($valueResponse) : null;
|
|
|
|
if ($candidate !== null && $candidate !== '') {
|
|
$rawValue = $candidate;
|
|
$sourceResponse = $valueResponse;
|
|
$point['alarm_value_id'] = $alarmMasterId;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($rawValue === null || $rawValue === '') {
|
|
$fallback = visuNormalizeAlarmValue(visuNormalizeScalarValue($point['value'] ?? $point['value_formatted'] ?? null));
|
|
|
|
if ($fallback === null || $fallback === '') {
|
|
if ($alarmMasterId > 0) {
|
|
$point['value'] = null;
|
|
$point['value_formatted'] = null;
|
|
$point['quality'] = 'unresolved';
|
|
$point['alarm_master_id'] = $alarmMasterId;
|
|
}
|
|
|
|
return $point;
|
|
}
|
|
|
|
$rawValue = $fallback;
|
|
}
|
|
|
|
$point['value'] = $rawValue;
|
|
$point['value_formatted'] = $rawValue;
|
|
|
|
$quality = is_array($sourceResponse) ? visuPickFirstScalar($sourceResponse, ['quality', 'Quality']) : null;
|
|
if ($quality !== null && $quality !== '') {
|
|
$point['quality'] = $quality;
|
|
}
|
|
|
|
$age = is_array($sourceResponse) ? visuPickFirstScalar($sourceResponse, ['age_s', 'age', 'Age']) : null;
|
|
if ($age !== null && $age !== '') {
|
|
$point['age_s'] = $age;
|
|
}
|
|
|
|
return $point;
|
|
}
|
|
|
|
function visuSourceKeyFromRef(array $ref): string
|
|
{
|
|
return implode('|', [
|
|
(string)($ref['type'] ?? $ref['variable_type'] ?? ''),
|
|
(string)($ref['external_id'] ?? ''),
|
|
(string)($ref['variable_id'] ?? ''),
|
|
(string)($ref['point_key'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
function visuCollectAlarmRefsFromValue($value, array &$sourceKeys, array &$variableIds): void
|
|
{
|
|
if (!is_array($value)) {
|
|
return;
|
|
}
|
|
|
|
$type = (string)($value['type'] ?? $value['variable_type'] ?? '');
|
|
|
|
if ($type === 'alarm') {
|
|
$sourceKey = visuSourceKeyFromRef($value);
|
|
|
|
if ($sourceKey !== '|||') {
|
|
$sourceKeys[$sourceKey] = true;
|
|
}
|
|
|
|
$variableId = (int)($value['variable_id'] ?? 0);
|
|
|
|
if ($variableId > 0) {
|
|
$variableIds[(string)$variableId] = true;
|
|
}
|
|
}
|
|
|
|
foreach ($value as $item) {
|
|
visuCollectAlarmRefsFromValue($item, $sourceKeys, $variableIds);
|
|
}
|
|
}
|
|
|
|
function visuCollectPageAlarmRefs(PDO $pdo, int $pageId): array
|
|
{
|
|
$sourceKeys = [];
|
|
$variableIds = [];
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT style_json, state_rules
|
|
FROM visu_elements
|
|
WHERE page_id = ?
|
|
");
|
|
$stmt->execute([$pageId]);
|
|
|
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
foreach (['style_json', 'state_rules'] as $column) {
|
|
$decoded = json_decode((string)($row[$column] ?? ''), true);
|
|
|
|
if (is_array($decoded)) {
|
|
visuCollectAlarmRefsFromValue($decoded, $sourceKeys, $variableIds);
|
|
}
|
|
}
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT r.variable_type, r.external_id, r.variable_id, r.point_key
|
|
FROM visu_element_state_rules r
|
|
INNER JOIN visu_elements e ON e.id = r.element_id
|
|
WHERE e.page_id = ?
|
|
AND r.variable_type = 'alarm'
|
|
");
|
|
$stmt->execute([$pageId]);
|
|
|
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
$sourceKey = visuSourceKeyFromRef($row);
|
|
|
|
if ($sourceKey !== '|||') {
|
|
$sourceKeys[$sourceKey] = true;
|
|
}
|
|
|
|
$variableId = (int)($row['variable_id'] ?? 0);
|
|
|
|
if ($variableId > 0) {
|
|
$variableIds[(string)$variableId] = true;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'source_keys' => $sourceKeys,
|
|
'variable_ids' => $variableIds,
|
|
];
|
|
}
|
|
|
|
function visuShouldHydrateAlarmPoint(array $point, array $alarmRefs): bool
|
|
{
|
|
if (($point['type'] ?? '') !== 'alarm') {
|
|
return false;
|
|
}
|
|
|
|
$sourceKey = (string)($point['source_key'] ?? visuPointSourceKey($point));
|
|
$variableId = (string)($point['variable_id'] ?? '');
|
|
|
|
return isset($alarmRefs['source_keys'][$sourceKey])
|
|
|| ($variableId !== '' && isset($alarmRefs['variable_ids'][$variableId]));
|
|
}
|
|
|
|
function visuLoadAssetStatesForAssets(PDO $pdo, array $assetIds): array
|
|
{
|
|
$assetIds = array_values(array_unique(array_filter(array_map('intval', $assetIds), fn($id) => $id > 0)));
|
|
|
|
if (empty($assetIds)) {
|
|
return [];
|
|
}
|
|
|
|
$ph = implode(',', array_fill(0, count($assetIds), '?'));
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT *
|
|
FROM visu_asset_states
|
|
WHERE asset_id IN ($ph)
|
|
ORDER BY asset_id ASC, is_default DESC, sort_order ASC, id ASC
|
|
");
|
|
$stmt->execute($assetIds);
|
|
|
|
$statesByAsset = [];
|
|
|
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
$statesByAsset[(int)$row['asset_id']][] = $row;
|
|
}
|
|
|
|
return $statesByAsset;
|
|
}
|
|
|
|
function visuGetProjectPoints(PDO $pdo, int $projectId, bool $hydrateAlarmValues = false): array
|
|
{
|
|
$stmt = $pdo->prepare("
|
|
SELECT isp_number, external_id, notes
|
|
FROM project_isps
|
|
WHERE project_id = ?
|
|
ORDER BY isp_number ASC
|
|
");
|
|
$stmt->execute([$projectId]);
|
|
$isps = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$allPoints = [];
|
|
|
|
foreach ($isps as $isp) {
|
|
$externalId = trim((string)($isp['external_id'] ?? ''));
|
|
|
|
if (!preg_match('/^\d{5}$/', $externalId)) {
|
|
continue;
|
|
}
|
|
|
|
foreach ([
|
|
'getanalog' => ['type' => 'analog', 'writable' => false],
|
|
'getdigital' => ['type' => 'digital', 'writable' => false],
|
|
'getalarms' => ['type' => 'alarm', 'writable' => false],
|
|
'getsetpoints' => ['type' => 'setpoint', 'writable' => true],
|
|
] as $fn => $meta) {
|
|
$upstream = visuCallMainApi($fn, $externalId);
|
|
$points = $upstream['points'] ?? $upstream['alarms'] ?? $upstream['setpoints'] ?? [];
|
|
|
|
if (!is_array($points)) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($points as $p) {
|
|
if (!is_array($p)) {
|
|
continue;
|
|
}
|
|
|
|
$key = trim((string)($p['key'] ?? ''));
|
|
$variableId = (int)($p['id'] ?? $p['variable_id'] ?? 0);
|
|
$alarmMasterId = $meta['type'] === 'alarm'
|
|
? (
|
|
visuObjectIdFromValue($p['value_raw'] ?? null)
|
|
?: visuObjectIdFromValue($p['value'] ?? null)
|
|
?: visuObjectIdFromValue($p['value_formatted'] ?? $p['formatted'] ?? null)
|
|
)
|
|
: 0;
|
|
|
|
if ($key === '' && $variableId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$point = [
|
|
'type' => $meta['type'],
|
|
'writable' => (bool)$meta['writable'],
|
|
'external_id' => $externalId,
|
|
'isp_number' => (int)$isp['isp_number'],
|
|
'variable_id' => $variableId > 0 ? $variableId : null,
|
|
'point_key' => $key,
|
|
'label' => visuPointLabel($p, (int)$isp['isp_number'], $meta['type']),
|
|
'raw_label' => (string)($p['label'] ?? ''),
|
|
'group' => (string)($p['group'] ?? ''),
|
|
'unit' => (string)($p['unit'] ?? ''),
|
|
'value' => $p['value'] ?? $p['value_raw'] ?? null,
|
|
'value_formatted' => $p['value_formatted'] ?? $p['formatted'] ?? $p['value_raw'] ?? $p['value'] ?? null,
|
|
'quality' => $p['quality'] ?? null,
|
|
'age_s' => $p['age_s'] ?? null,
|
|
'alarm_master_id' => $alarmMasterId > 0 ? $alarmMasterId : null,
|
|
];
|
|
|
|
if ($hydrateAlarmValues && $meta['type'] === 'alarm') {
|
|
$point = visuHydrateAlarmPointValue($point);
|
|
}
|
|
|
|
$point['source_key'] = visuPointSourceKey($point);
|
|
$allPoints[] = $point;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $allPoints;
|
|
}
|
|
|
|
function visuBuildLiveValueMap(array $points): array
|
|
{
|
|
$values = [];
|
|
|
|
foreach ($points as $point) {
|
|
if (!is_array($point)) {
|
|
continue;
|
|
}
|
|
|
|
$sourceKey = (string)($point['source_key'] ?? '');
|
|
|
|
if ($sourceKey !== '') {
|
|
$values[$sourceKey] = $point;
|
|
}
|
|
|
|
if (!empty($point['variable_id'])) {
|
|
$variableKey = (string)$point['variable_id'];
|
|
|
|
if (
|
|
!isset($values[$variableKey])
|
|
|| !visuPointHasDisplayValue($values[$variableKey])
|
|
|| visuPointHasDisplayValue($point)
|
|
) {
|
|
$values[$variableKey] = $point;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
|
|
function visuVirtualPointsTableReady(PDO $pdo): bool
|
|
{
|
|
static $ready = null;
|
|
|
|
if ($ready !== null) {
|
|
return $ready;
|
|
}
|
|
|
|
try {
|
|
$pdo->query("SELECT 1 FROM visu_virtual_points LIMIT 1");
|
|
$ready = true;
|
|
} catch (Throwable $e) {
|
|
$ready = false;
|
|
}
|
|
|
|
return $ready;
|
|
}
|
|
|
|
function visuVirtualPointSourceKey(int $projectId, string $pointKey): string
|
|
{
|
|
return implode('|', ['virtual', 'vdp:' . $projectId, '', $pointKey]);
|
|
}
|
|
|
|
function visuVirtualPointNumericValue($value): ?float
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
if (is_bool($value)) {
|
|
return $value ? 1.0 : 0.0;
|
|
}
|
|
|
|
if (is_int($value) || is_float($value)) {
|
|
return (float)$value;
|
|
}
|
|
|
|
$text = trim((string)$value);
|
|
|
|
if ($text === '') {
|
|
return null;
|
|
}
|
|
|
|
$text = str_replace(',', '.', $text);
|
|
|
|
if (preg_match('/-?\d+(?:\.\d+)?/', $text, $match)) {
|
|
return (float)$match[0];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function visuVirtualPointFormat(?float $value, int $decimals, string $unit = ''): ?string
|
|
{
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
|
|
$precision = max(0, min(8, $decimals));
|
|
$formatted = number_format($value, $precision, ',', '.');
|
|
$unit = trim($unit);
|
|
|
|
return $unit !== '' ? ($formatted . ' ' . $unit) : $formatted;
|
|
}
|
|
|
|
function visuVirtualPointRows(PDO $pdo, int $projectId): array
|
|
{
|
|
if (!visuVirtualPointsTableReady($pdo)) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
$stmt = $pdo->prepare("
|
|
SELECT *
|
|
FROM visu_virtual_points
|
|
WHERE project_id = ?
|
|
AND is_active = 1
|
|
ORDER BY sort_order ASC, name ASC, id ASC
|
|
");
|
|
$stmt->execute([$projectId]);
|
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (Throwable $e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function visuVirtualPointRefFromToken(array $token): ?array
|
|
{
|
|
$type = (string)($token['type'] ?? '');
|
|
|
|
if ($type !== 'point' && !is_array($token['ref'] ?? null)) {
|
|
return null;
|
|
}
|
|
|
|
$ref = is_array($token['ref'] ?? null) ? $token['ref'] : [];
|
|
|
|
foreach (['type', 'variable_type', 'external_id', 'variable_id', 'point_key', 'point_label', 'label', 'source_key'] as $key) {
|
|
if (array_key_exists($key, $token) && !array_key_exists($key, $ref)) {
|
|
$ref[$key] = $token[$key];
|
|
}
|
|
}
|
|
|
|
return !empty($ref) ? $ref : null;
|
|
}
|
|
|
|
function visuVirtualPointTokensToRpn(array $tokens): array
|
|
{
|
|
$output = [];
|
|
$stack = [];
|
|
$precedence = ['+' => 1, '-' => 1, '*' => 2, '/' => 2];
|
|
|
|
foreach ($tokens as $token) {
|
|
if (!is_array($token)) {
|
|
continue;
|
|
}
|
|
|
|
$type = (string)($token['type'] ?? '');
|
|
|
|
if ($type === 'number') {
|
|
$value = visuVirtualPointNumericValue($token['value'] ?? null);
|
|
|
|
if ($value === null) {
|
|
continue;
|
|
}
|
|
|
|
$output[] = ['kind' => 'number', 'value' => $value];
|
|
continue;
|
|
}
|
|
|
|
if ($type === 'point') {
|
|
$ref = visuVirtualPointRefFromToken($token);
|
|
|
|
if ($ref) {
|
|
$output[] = ['kind' => 'point', 'ref' => $ref];
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($type !== 'operator') {
|
|
continue;
|
|
}
|
|
|
|
$value = trim((string)($token['value'] ?? ''));
|
|
|
|
if ($value === '') {
|
|
continue;
|
|
}
|
|
|
|
if ($value === '(') {
|
|
$stack[] = $value;
|
|
continue;
|
|
}
|
|
|
|
if ($value === ')') {
|
|
while (!empty($stack) && end($stack) !== '(') {
|
|
$output[] = ['kind' => 'operator', 'value' => array_pop($stack)];
|
|
}
|
|
|
|
if (!empty($stack) && end($stack) === '(') {
|
|
array_pop($stack);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!isset($precedence[$value])) {
|
|
continue;
|
|
}
|
|
|
|
while (!empty($stack)) {
|
|
$top = end($stack);
|
|
|
|
if ($top === '(' || !isset($precedence[$top]) || $precedence[$top] < $precedence[$value]) {
|
|
break;
|
|
}
|
|
|
|
$output[] = ['kind' => 'operator', 'value' => array_pop($stack)];
|
|
}
|
|
|
|
$stack[] = $value;
|
|
}
|
|
|
|
while (!empty($stack)) {
|
|
$operator = array_pop($stack);
|
|
|
|
if ($operator === '(' || $operator === ')') {
|
|
continue;
|
|
}
|
|
|
|
$output[] = ['kind' => 'operator', 'value' => $operator];
|
|
}
|
|
|
|
return $output;
|
|
}
|
|
|
|
function visuEvaluateVirtualPointRow(array $row, array &$context): array
|
|
{
|
|
$pointKey = trim((string)($row['point_key'] ?? ''));
|
|
|
|
if ($pointKey === '') {
|
|
return ['ok' => false, 'value' => null, 'error' => 'VDP ohne Punkt-Key.'];
|
|
}
|
|
|
|
if (isset($context['cache'][$pointKey])) {
|
|
return $context['cache'][$pointKey];
|
|
}
|
|
|
|
if (!empty($context['visiting'][$pointKey])) {
|
|
return ['ok' => false, 'value' => null, 'error' => 'Zyklische VDP-Referenz: ' . $pointKey];
|
|
}
|
|
|
|
$context['visiting'][$pointKey] = true;
|
|
|
|
$tokens = json_decode((string)($row['formula_json'] ?? '[]'), true);
|
|
|
|
if (!is_array($tokens)) {
|
|
unset($context['visiting'][$pointKey]);
|
|
return ['ok' => false, 'value' => null, 'error' => 'Ungültige Formel.'];
|
|
}
|
|
|
|
$rpn = visuVirtualPointTokensToRpn($tokens);
|
|
|
|
if (empty($rpn)) {
|
|
unset($context['visiting'][$pointKey]);
|
|
$result = ['ok' => false, 'value' => null, 'error' => 'Leere Formel.'];
|
|
$context['cache'][$pointKey] = $result;
|
|
return $result;
|
|
}
|
|
|
|
$stack = [];
|
|
|
|
foreach ($rpn as $token) {
|
|
if (($token['kind'] ?? '') === 'number') {
|
|
$stack[] = (float)$token['value'];
|
|
continue;
|
|
}
|
|
|
|
if (($token['kind'] ?? '') === 'point') {
|
|
$ref = is_array($token['ref'] ?? null) ? $token['ref'] : null;
|
|
|
|
if (!$ref) {
|
|
unset($context['visiting'][$pointKey]);
|
|
return ['ok' => false, 'value' => null, 'error' => 'Ungültige VDP-Quelle.'];
|
|
}
|
|
|
|
$refType = (string)($ref['type'] ?? $ref['variable_type'] ?? '');
|
|
$numericValue = null;
|
|
|
|
if ($refType === 'virtual' || str_starts_with((string)($ref['external_id'] ?? ''), 'vdp:')) {
|
|
$virtualPointKey = trim((string)($ref['point_key'] ?? ''));
|
|
$virtualRow = $virtualPointKey !== '' ? ($context['rows'][$virtualPointKey] ?? null) : null;
|
|
|
|
if (!$virtualRow) {
|
|
unset($context['visiting'][$pointKey]);
|
|
return ['ok' => false, 'value' => null, 'error' => 'VDP-Quelle nicht gefunden: ' . $virtualPointKey];
|
|
}
|
|
|
|
$virtualResult = visuEvaluateVirtualPointRow($virtualRow, $context);
|
|
|
|
if (empty($virtualResult['ok'])) {
|
|
unset($context['visiting'][$pointKey]);
|
|
return ['ok' => false, 'value' => null, 'error' => (string)($virtualResult['error'] ?? 'VDP-Fehler')];
|
|
}
|
|
|
|
$numericValue = $virtualResult['value'];
|
|
} else {
|
|
$sourceKey = !empty($ref['source_key']) ? (string)$ref['source_key'] : visuSourceKeyFromRef($ref);
|
|
$point = $context['points'][$sourceKey] ?? null;
|
|
|
|
if (!$point && !empty($ref['variable_id'])) {
|
|
$point = $context['points'][(string)$ref['variable_id']] ?? null;
|
|
}
|
|
|
|
$numericValue = $point ? visuVirtualPointNumericValue($point['value'] ?? $point['value_formatted'] ?? null) : null;
|
|
}
|
|
|
|
if ($numericValue === null) {
|
|
unset($context['visiting'][$pointKey]);
|
|
return ['ok' => false, 'value' => null, 'error' => 'Quelle ohne numerischen Wert.'];
|
|
}
|
|
|
|
$stack[] = $numericValue;
|
|
continue;
|
|
}
|
|
|
|
if (($token['kind'] ?? '') === 'operator') {
|
|
if (count($stack) < 2) {
|
|
unset($context['visiting'][$pointKey]);
|
|
return ['ok' => false, 'value' => null, 'error' => 'Unvollständige Formel.'];
|
|
}
|
|
|
|
$right = array_pop($stack);
|
|
$left = array_pop($stack);
|
|
|
|
switch ((string)($token['value'] ?? '')) {
|
|
case '+':
|
|
$stack[] = $left + $right;
|
|
break;
|
|
case '-':
|
|
$stack[] = $left - $right;
|
|
break;
|
|
case '*':
|
|
$stack[] = $left * $right;
|
|
break;
|
|
case '/':
|
|
if ((float)$right === 0.0) {
|
|
unset($context['visiting'][$pointKey]);
|
|
return ['ok' => false, 'value' => null, 'error' => 'Division durch 0.'];
|
|
}
|
|
$stack[] = $left / $right;
|
|
break;
|
|
default:
|
|
unset($context['visiting'][$pointKey]);
|
|
return ['ok' => false, 'value' => null, 'error' => 'Unbekannter Operator.'];
|
|
}
|
|
}
|
|
}
|
|
|
|
unset($context['visiting'][$pointKey]);
|
|
|
|
if (count($stack) !== 1) {
|
|
$result = ['ok' => false, 'value' => null, 'error' => 'Formel konnte nicht ausgewertet werden.'];
|
|
$context['cache'][$pointKey] = $result;
|
|
return $result;
|
|
}
|
|
|
|
$result = ['ok' => true, 'value' => (float)$stack[0], 'error' => ''];
|
|
$context['cache'][$pointKey] = $result;
|
|
return $result;
|
|
}
|
|
|
|
function visuGetProjectVirtualPoints(PDO $pdo, int $projectId, array $basePoints): array
|
|
{
|
|
$rows = visuVirtualPointRows($pdo, $projectId);
|
|
|
|
if (empty($rows)) {
|
|
return [];
|
|
}
|
|
|
|
$rowsByPointKey = [];
|
|
|
|
foreach ($rows as $row) {
|
|
$pointKey = trim((string)($row['point_key'] ?? ''));
|
|
|
|
if ($pointKey !== '') {
|
|
$rowsByPointKey[$pointKey] = $row;
|
|
}
|
|
}
|
|
|
|
$context = [
|
|
'rows' => $rowsByPointKey,
|
|
'points' => visuBuildLiveValueMap($basePoints),
|
|
'cache' => [],
|
|
'visiting' => [],
|
|
];
|
|
|
|
$virtualPoints = [];
|
|
|
|
foreach ($rows as $row) {
|
|
$pointKey = trim((string)($row['point_key'] ?? ''));
|
|
|
|
if ($pointKey === '') {
|
|
continue;
|
|
}
|
|
|
|
$result = visuEvaluateVirtualPointRow($row, $context);
|
|
$value = !empty($result['ok']) ? $result['value'] : null;
|
|
$unit = trim((string)($row['unit'] ?? ''));
|
|
$decimals = (int)($row['decimals'] ?? 2);
|
|
|
|
$virtualPoints[] = [
|
|
'type' => 'virtual',
|
|
'writable' => false,
|
|
'external_id' => 'vdp:' . $projectId,
|
|
'isp_number' => 0,
|
|
'variable_id' => null,
|
|
'point_key' => $pointKey,
|
|
'label' => trim((string)($row['name'] ?? $pointKey)),
|
|
'raw_label' => trim((string)($row['name'] ?? $pointKey)),
|
|
'group' => 'Virtuelle Datenpunkte',
|
|
'unit' => $unit,
|
|
'value' => $value,
|
|
'value_formatted' => visuVirtualPointFormat($value, $decimals, $unit),
|
|
'quality' => !empty($result['ok']) ? 'virtual' : 'error',
|
|
'age_s' => 0,
|
|
'source_key' => visuVirtualPointSourceKey($projectId, $pointKey),
|
|
'description' => trim((string)($row['description'] ?? '')),
|
|
'virtual_error' => empty($result['ok']) ? (string)($result['error'] ?? 'VDP Fehler') : '',
|
|
];
|
|
}
|
|
|
|
return $virtualPoints;
|
|
}
|
|
|
|
function visuGetProjectPointsWithVirtual(PDO $pdo, int $projectId, bool $hydrateAlarmValues = false): array
|
|
{
|
|
$points = visuGetProjectPoints($pdo, $projectId, $hydrateAlarmValues);
|
|
$virtualPoints = visuGetProjectVirtualPoints($pdo, $projectId, $points);
|
|
|
|
return array_merge($points, $virtualPoints);
|
|
}
|
|
|
|
function visuAllowedInfluxDatabases(PDO $pdo, int $userId, bool $canViewAll): array
|
|
{
|
|
if ($canViewAll) {
|
|
$stmt = $pdo->query("
|
|
SELECT DISTINCT database_name
|
|
FROM role_influx_databases
|
|
WHERE database_name <> ''
|
|
ORDER BY database_name ASC
|
|
");
|
|
|
|
return array_values(array_filter(array_map('strval', $stmt->fetchAll(PDO::FETCH_COLUMN))));
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT DISTINCT rid.database_name
|
|
FROM role_influx_databases rid
|
|
INNER JOIN user_roles ur ON ur.role_id = rid.role_id
|
|
WHERE ur.user_id = ?
|
|
AND rid.database_name <> ''
|
|
ORDER BY rid.database_name ASC
|
|
");
|
|
$stmt->execute([$userId]);
|
|
|
|
return array_values(array_filter(array_map('strval', $stmt->fetchAll(PDO::FETCH_COLUMN))));
|
|
}
|
|
|
|
function visuInfluxIdentifier(string $value): string
|
|
{
|
|
return '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $value) . '"';
|
|
}
|
|
|
|
function visuInfluxString(string $value): string
|
|
{
|
|
return "'" . str_replace(['\\', "'"], ['\\\\', "\\'"], $value) . "'";
|
|
}
|
|
|
|
function visuInfluxQuery(string $database, string $query): array
|
|
{
|
|
$url = rtrim(VISU_INFLUX_URL, '/') . '/query?' . http_build_query([
|
|
'db' => $database,
|
|
'q' => $query,
|
|
]);
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 12);
|
|
|
|
$resp = curl_exec($ch);
|
|
$err = curl_error($ch);
|
|
$status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($err) {
|
|
return ['ok' => false, 'error' => $err];
|
|
}
|
|
|
|
$decoded = json_decode((string)$resp, true);
|
|
|
|
if (!is_array($decoded)) {
|
|
return ['ok' => false, 'error' => 'Influx liefert kein JSON.', 'raw' => (string)$resp];
|
|
}
|
|
|
|
if ($status >= 400) {
|
|
return ['ok' => false, 'error' => $decoded['error'] ?? ('Influx HTTP ' . $status)];
|
|
}
|
|
|
|
$result = $decoded['results'][0] ?? [];
|
|
|
|
if (!empty($result['error'])) {
|
|
return ['ok' => false, 'error' => (string)$result['error']];
|
|
}
|
|
|
|
return ['ok' => true, 'result' => $result];
|
|
}
|
|
|
|
function visuInfluxSeriesValues(array $response): array
|
|
{
|
|
$series = $response['result']['series'][0] ?? null;
|
|
|
|
if (!$series || empty($series['values']) || !is_array($series['values'])) {
|
|
return [];
|
|
}
|
|
|
|
return $series['values'];
|
|
}
|
|
|
|
function visuInfluxListMeasurements(string $database): array
|
|
{
|
|
$response = visuInfluxQuery($database, 'SHOW MEASUREMENTS');
|
|
|
|
if (empty($response['ok'])) {
|
|
return [];
|
|
}
|
|
|
|
return array_values(array_filter(array_map(
|
|
fn($row) => (string)($row[0] ?? ''),
|
|
visuInfluxSeriesValues($response)
|
|
)));
|
|
}
|
|
|
|
function visuInfluxListTrendPoints(string $database): array
|
|
{
|
|
$points = [];
|
|
$measurements = visuInfluxListMeasurements($database);
|
|
|
|
foreach ($measurements as $measurement) {
|
|
$tagKeyResponse = visuInfluxQuery($database, 'SHOW TAG KEYS FROM ' . visuInfluxIdentifier($measurement));
|
|
|
|
if (empty($tagKeyResponse['ok'])) {
|
|
continue;
|
|
}
|
|
|
|
foreach (visuInfluxSeriesValues($tagKeyResponse) as $tagKeyRow) {
|
|
$tagKey = (string)($tagKeyRow[0] ?? '');
|
|
|
|
if ($tagKey === '') {
|
|
continue;
|
|
}
|
|
|
|
$tagValueResponse = visuInfluxQuery(
|
|
$database,
|
|
'SHOW TAG VALUES FROM ' . visuInfluxIdentifier($measurement) . ' WITH KEY = ' . visuInfluxIdentifier($tagKey)
|
|
);
|
|
|
|
if (empty($tagValueResponse['ok'])) {
|
|
continue;
|
|
}
|
|
|
|
foreach (visuInfluxSeriesValues($tagValueResponse) as $tagValueRow) {
|
|
$value = (string)($tagValueRow[1] ?? $tagValueRow[0] ?? '');
|
|
|
|
if ($value === '') {
|
|
continue;
|
|
}
|
|
|
|
$points[] = [
|
|
'database' => $database,
|
|
'measurement' => $measurement,
|
|
'tag_key' => $tagKey,
|
|
'tag_value' => $value,
|
|
'label' => $measurement . ' · ' . $value,
|
|
'source_key' => implode('|', ['influx', $database, $measurement, $tagKey, $value]),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
usort($points, fn($a, $b) => strcmp($a['label'], $b['label']));
|
|
return $points;
|
|
}
|
|
|
|
function visuTrendRange(string $range): array
|
|
{
|
|
$ranges = [
|
|
'15m' => ['sql' => '15m', 'seconds' => 900],
|
|
'1h' => ['sql' => '1h', 'seconds' => 3600],
|
|
'6h' => ['sql' => '6h', 'seconds' => 21600],
|
|
'12h' => ['sql' => '12h', 'seconds' => 43200],
|
|
'24h' => ['sql' => '24h', 'seconds' => 86400],
|
|
'7d' => ['sql' => '7d', 'seconds' => 604800],
|
|
'30d' => ['sql' => '30d', 'seconds' => 2592000],
|
|
'90d' => ['sql' => '90d', 'seconds' => 7776000],
|
|
'180d' => ['sql' => '180d', 'seconds' => 15552000],
|
|
];
|
|
|
|
return $ranges[$range] ?? $ranges['24h'];
|
|
}
|
|
|
|
function visuTrendCustomRange($start, $end): ?array
|
|
{
|
|
if ($start === null || $end === null || $start === '' || $end === '') {
|
|
return null;
|
|
}
|
|
|
|
$startTs = is_numeric($start) ? ((float)$start / 1000) : strtotime((string)$start);
|
|
$endTs = is_numeric($end) ? ((float)$end / 1000) : strtotime((string)$end);
|
|
|
|
if (!$startTs || !$endTs) {
|
|
return null;
|
|
}
|
|
|
|
if ($endTs < $startTs) {
|
|
[$startTs, $endTs] = [$endTs, $startTs];
|
|
}
|
|
|
|
if (($endTs - $startTs) < 1) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'sql' => sprintf(
|
|
"time >= '%s' AND time <= '%s'",
|
|
gmdate('Y-m-d\TH:i:s\Z', (int)$startTs),
|
|
gmdate('Y-m-d\TH:i:s\Z', (int)$endTs)
|
|
),
|
|
'seconds' => max(1, (int)round($endTs - $startTs)),
|
|
'start' => gmdate('c', (int)$startTs),
|
|
'end' => gmdate('c', (int)$endTs),
|
|
];
|
|
}
|
|
|
|
function visuTrendInterval(string $interval, int $rangeSeconds): string
|
|
{
|
|
$allowed = ['10s', '30s', '1m', '5m', '10m', '15m', '30m', '1h', '6h', '12h', '1d'];
|
|
|
|
if ($interval !== 'auto' && in_array($interval, $allowed, true)) {
|
|
return $interval;
|
|
}
|
|
|
|
if ($rangeSeconds <= 900) return '10s';
|
|
if ($rangeSeconds <= 3600) return '30s';
|
|
if ($rangeSeconds <= 21600) return '5m';
|
|
if ($rangeSeconds <= 86400) return '10m';
|
|
if ($rangeSeconds <= 604800) return '1h';
|
|
return '6h';
|
|
}
|
|
|
|
function visuTrendAggregate(string $aggregate): string
|
|
{
|
|
return in_array($aggregate, ['mean', 'min', 'max', 'last'], true) ? $aggregate : 'mean';
|
|
}
|
|
|
|
function visuInfluxReadTrendSeries(array $series, array $allowedDatabases, string $range, string $interval, string $aggregate, $start = null, $end = null): array
|
|
{
|
|
$customRange = visuTrendCustomRange($start, $end);
|
|
$rangeInfo = $customRange ?: visuTrendRange($range);
|
|
$bucket = visuTrendInterval($interval, (int)$rangeInfo['seconds']);
|
|
$agg = visuTrendAggregate($aggregate);
|
|
$out = [];
|
|
|
|
foreach (array_slice($series, 0, 8) as $index => $item) {
|
|
if (!is_array($item)) {
|
|
continue;
|
|
}
|
|
|
|
$database = (string)($item['database'] ?? '');
|
|
$measurement = (string)($item['measurement'] ?? '');
|
|
$tagKey = (string)($item['tag_key'] ?? '');
|
|
$tagValue = (string)($item['tag_value'] ?? '');
|
|
|
|
if ($database === '' || $measurement === '' || $tagKey === '' || $tagValue === '') {
|
|
continue;
|
|
}
|
|
|
|
if (!in_array($database, $allowedDatabases, true)) {
|
|
continue;
|
|
}
|
|
|
|
$query = sprintf(
|
|
'SELECT %s("value") FROM %s WHERE %s = %s AND %s GROUP BY time(%s) fill(null)',
|
|
$agg,
|
|
visuInfluxIdentifier($measurement),
|
|
visuInfluxIdentifier($tagKey),
|
|
visuInfluxString($tagValue),
|
|
$customRange ? $rangeInfo['sql'] : 'time >= now() - ' . $rangeInfo['sql'],
|
|
$bucket
|
|
);
|
|
|
|
$response = visuInfluxQuery($database, $query);
|
|
$values = [];
|
|
|
|
if (!empty($response['ok'])) {
|
|
foreach (visuInfluxSeriesValues($response) as $row) {
|
|
$values[] = [
|
|
'time' => (string)($row[0] ?? ''),
|
|
'value' => $row[1] ?? null,
|
|
];
|
|
}
|
|
}
|
|
|
|
$out[] = [
|
|
'id' => (string)($item['id'] ?? ('series-' . $index)),
|
|
'label' => (string)($item['label'] ?? $tagValue),
|
|
'color' => (string)($item['color'] ?? '#38bdf8'),
|
|
'database' => $database,
|
|
'measurement' => $measurement,
|
|
'tag_key' => $tagKey,
|
|
'tag_value' => $tagValue,
|
|
'values' => $values,
|
|
'error' => empty($response['ok']) ? (string)($response['error'] ?? 'Influx Fehler') : '',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'range' => $customRange ? 'custom' : $range,
|
|
'interval' => $bucket,
|
|
'aggregate' => $agg,
|
|
'start' => $customRange['start'] ?? null,
|
|
'end' => $customRange['end'] ?? null,
|
|
'series' => $out,
|
|
];
|
|
}
|
|
|
|
function visuInfluxReadLatestValues(array $series, array $allowedDatabases): array
|
|
{
|
|
$out = [];
|
|
|
|
foreach (array_slice($series, 0, 64) as $index => $item) {
|
|
if (!is_array($item)) {
|
|
continue;
|
|
}
|
|
|
|
$database = (string)($item['database'] ?? '');
|
|
$measurement = (string)($item['measurement'] ?? '');
|
|
$tagKey = (string)($item['tag_key'] ?? '');
|
|
$tagValue = (string)($item['tag_value'] ?? '');
|
|
|
|
if ($database === '' || $measurement === '' || $tagKey === '' || $tagValue === '') {
|
|
continue;
|
|
}
|
|
|
|
if (!in_array($database, $allowedDatabases, true)) {
|
|
continue;
|
|
}
|
|
|
|
$query = sprintf(
|
|
'SELECT last("value") FROM %s WHERE %s = %s',
|
|
visuInfluxIdentifier($measurement),
|
|
visuInfluxIdentifier($tagKey),
|
|
visuInfluxString($tagValue)
|
|
);
|
|
|
|
$response = visuInfluxQuery($database, $query);
|
|
$value = null;
|
|
|
|
if (!empty($response['ok'])) {
|
|
$rows = visuInfluxSeriesValues($response);
|
|
$value = $rows[0][1] ?? null;
|
|
}
|
|
|
|
$out[] = [
|
|
'id' => (string)($item['id'] ?? ('series-' . $index)),
|
|
'database' => $database,
|
|
'measurement' => $measurement,
|
|
'tag_key' => $tagKey,
|
|
'tag_value' => $tagValue,
|
|
'value' => $value,
|
|
'error' => empty($response['ok']) ? (string)($response['error'] ?? 'Influx Fehler') : '',
|
|
];
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
function visuCsvDelimiter($value): string
|
|
{
|
|
$value = (string)$value;
|
|
return in_array($value, [',', ';', "\t"], true) ? $value : ';';
|
|
}
|
|
|
|
function visuCsvExportKind(string $value): string
|
|
{
|
|
return in_array($value, ['trend_csv', 'lastvalues_csv'], true) ? $value : 'trend_csv';
|
|
}
|
|
|
|
function visuCsvScheduleKind(string $value): string
|
|
{
|
|
return in_array($value, ['interval', 'daily', 'weekly', 'monthly'], true) ? $value : 'daily';
|
|
}
|
|
|
|
function visuCsvTimeOfDay(string $value): string
|
|
{
|
|
$value = trim($value);
|
|
if (!preg_match('/^(\d{2}):(\d{2})$/', $value, $m)) {
|
|
return '00:00';
|
|
}
|
|
|
|
$hour = max(0, min(23, (int)$m[1]));
|
|
$minute = max(0, min(59, (int)$m[2]));
|
|
return sprintf('%02d:%02d', $hour, $minute);
|
|
}
|
|
|
|
function visuCsvWeekdays(string $value): string
|
|
{
|
|
$items = array_filter(array_map('trim', explode(',', $value)), static fn($item) => $item !== '');
|
|
$days = [];
|
|
|
|
foreach ($items as $item) {
|
|
$day = (int)$item;
|
|
if ($day >= 1 && $day <= 7) {
|
|
$days[] = $day;
|
|
}
|
|
}
|
|
|
|
$days = array_values(array_unique($days));
|
|
sort($days, SORT_NUMERIC);
|
|
|
|
return implode(',', $days);
|
|
}
|
|
|
|
function visuCsvIntervalMinutes($value): int
|
|
{
|
|
return max(1, min(525600, (int)$value));
|
|
}
|
|
|
|
function visuCsvMonthDay($value): int
|
|
{
|
|
return max(1, min(31, (int)$value));
|
|
}
|
|
|
|
function visuCsvRetentionDays($value): int
|
|
{
|
|
return max(1, min(3650, (int)$value));
|
|
}
|
|
|
|
function visuCsvTargetDir(string $value): string
|
|
{
|
|
$value = str_replace('\\', '/', trim($value));
|
|
$parts = array_filter(explode('/', $value), static function ($part) {
|
|
return $part !== '' && $part !== '.' && $part !== '..';
|
|
});
|
|
|
|
$clean = [];
|
|
foreach ($parts as $part) {
|
|
$cleanPart = preg_replace('/[^a-zA-Z0-9._-]+/', '_', $part);
|
|
$cleanPart = trim((string)$cleanPart, '._-');
|
|
if ($cleanPart !== '') {
|
|
$clean[] = $cleanPart;
|
|
}
|
|
}
|
|
|
|
return !empty($clean) ? implode('/', $clean) : 'csv_exports';
|
|
}
|
|
|
|
function visuCsvFilenamePattern(string $value): string
|
|
{
|
|
$value = trim($value);
|
|
return $value !== '' ? mb_substr($value, 0, 255) : '{selection}_{yyyy}-{mm}-{dd}_{hh}-{ii}-{ss}.csv';
|
|
}
|
|
|
|
function visuCsvNextRunAt(
|
|
string $scheduleKind,
|
|
int $intervalMinutes,
|
|
string $timeOfDay,
|
|
string $weekdays,
|
|
int $monthDay,
|
|
?DateTimeImmutable $now = null
|
|
): string {
|
|
$now = $now ?: new DateTimeImmutable('now');
|
|
[$hour, $minute] = array_map('intval', explode(':', visuCsvTimeOfDay($timeOfDay)));
|
|
$scheduleKind = visuCsvScheduleKind($scheduleKind);
|
|
|
|
if ($scheduleKind === 'interval') {
|
|
return $now->modify('+' . visuCsvIntervalMinutes($intervalMinutes) . ' minutes')->format('Y-m-d H:i:s');
|
|
}
|
|
|
|
if ($scheduleKind === 'daily') {
|
|
$candidate = $now->setTime($hour, $minute, 0);
|
|
if ($candidate <= $now) {
|
|
$candidate = $candidate->modify('+1 day');
|
|
}
|
|
return $candidate->format('Y-m-d H:i:s');
|
|
}
|
|
|
|
if ($scheduleKind === 'weekly') {
|
|
$days = array_filter(array_map('intval', explode(',', visuCsvWeekdays($weekdays))), static fn($day) => $day >= 1 && $day <= 7);
|
|
if (empty($days)) {
|
|
$days = [(int)$now->format('N')];
|
|
}
|
|
|
|
for ($offset = 0; $offset <= 14; $offset++) {
|
|
$candidateDay = $now->modify('+' . $offset . ' days');
|
|
if (!in_array((int)$candidateDay->format('N'), $days, true)) {
|
|
continue;
|
|
}
|
|
|
|
$candidate = $candidateDay->setTime($hour, $minute, 0);
|
|
if ($candidate > $now) {
|
|
return $candidate->format('Y-m-d H:i:s');
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($scheduleKind === 'monthly') {
|
|
$monthDay = visuCsvMonthDay($monthDay);
|
|
$base = $now;
|
|
|
|
for ($i = 0; $i < 14; $i++) {
|
|
$monthStart = $base->modify('first day of this month')->setTime($hour, $minute, 0);
|
|
$daysInMonth = (int)$monthStart->format('t');
|
|
$day = min($monthDay, $daysInMonth);
|
|
$candidate = $monthStart->setDate(
|
|
(int)$monthStart->format('Y'),
|
|
(int)$monthStart->format('m'),
|
|
$day
|
|
);
|
|
|
|
if ($candidate > $now) {
|
|
return $candidate->format('Y-m-d H:i:s');
|
|
}
|
|
|
|
$base = $base->modify('first day of next month');
|
|
}
|
|
}
|
|
|
|
$fallback = $now->modify('+1 day')->setTime($hour, $minute, 0);
|
|
return $fallback->format('Y-m-d H:i:s');
|
|
}
|
|
|
|
function visuCsvExportJobsTableReady(PDO $pdo): bool
|
|
{
|
|
try {
|
|
$pdo->query('SELECT 1 FROM visu_csv_export_jobs LIMIT 1');
|
|
return true;
|
|
} catch (Throwable $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function visuCsvExportFilesTableReady(PDO $pdo): bool
|
|
{
|
|
try {
|
|
$pdo->query('SELECT 1 FROM visu_csv_export_files LIMIT 1');
|
|
return true;
|
|
} catch (Throwable $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function visuCsvExportAbsolutePath(string $relativePath): string
|
|
{
|
|
$relativePath = str_replace('\\', '/', trim($relativePath));
|
|
$relativePath = preg_replace('#/+#', '/', $relativePath);
|
|
$relativePath = ltrim((string)$relativePath, '/');
|
|
return VISU_EXPORT_STORAGE_ROOT . '/' . $relativePath;
|
|
}
|
|
|
|
$username = (string)($_SESSION['username'] ?? '');
|
|
$userId = resolveUserId($pdo, (int)($_SESSION['user_id'] ?? 0), $username);
|
|
$groups = visuUserGroups($pdo, $username, $_SESSION['groups'] ?? []);
|
|
|
|
$action = (string)($_GET['action'] ?? $_POST['action'] ?? '');
|
|
|
|
$canViewViewer = hasPermission($pdo, $userId, 'page.visu.viewer.view');
|
|
$canViewAll = hasPermission($pdo, $userId, 'page.visu.viewer.viewall');
|
|
$canViewDesigner = hasPermission($pdo, $userId, 'page.visu.designer.view');
|
|
$canEditDesigner = hasPermission($pdo, $userId, 'page.visu.designer.edit');
|
|
$canManageAssets = hasPermission($pdo, $userId, 'page.visu.assets.manage');
|
|
$canEditAssets = $canManageAssets || hasPermission($pdo, $userId, 'page.visu.assets.edit');
|
|
$canDeleteAssets = $canManageAssets || hasPermission($pdo, $userId, 'page.visu.assets.delete');
|
|
$canWriteSetpoints = hasPermission($pdo, $userId, 'page.visu.setpoints.write');
|
|
|
|
if (!$canViewViewer && !$canViewDesigner) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Zugriff auf SE-Cloud Vis.'], 403);
|
|
}
|
|
|
|
if ($action === 'projects') {
|
|
$params = [];
|
|
$visSql = visuVisibilitySql($userId, $canViewAll, 'p.id', $params, 'projects');
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
p.id,
|
|
p.year,
|
|
p.project_number,
|
|
p.name,
|
|
pc.company_name AS customer_name
|
|
FROM projects p
|
|
LEFT JOIN project_customers pc ON pc.id = p.customer_id
|
|
WHERE {$visSql}
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM project_isps pi
|
|
WHERE pi.project_id = p.id
|
|
)
|
|
ORDER BY p.year DESC, p.project_number ASC, p.name ASC
|
|
");
|
|
$stmt->execute($params);
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'projects' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
|
]);
|
|
}
|
|
|
|
if ($action === 'virtual_points') {
|
|
$projectId = (int)($_GET['project_id'] ?? 0);
|
|
|
|
if ($projectId <= 0) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt fehlt.'], 400);
|
|
}
|
|
|
|
if (!visuProjectVisible($pdo, $projectId, $userId, $canViewAll)) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt nicht sichtbar.'], 403);
|
|
}
|
|
|
|
if (!visuVirtualPointsTableReady($pdo)) {
|
|
visuJson([
|
|
'ok' => false,
|
|
'error' => 'Tabelle visu_virtual_points fehlt. Bitte SQL-Migration ausfuehren.',
|
|
], 500);
|
|
}
|
|
|
|
$rows = visuVirtualPointRows($pdo, $projectId);
|
|
|
|
foreach ($rows as &$row) {
|
|
$formula = json_decode((string)($row['formula_json'] ?? '[]'), true);
|
|
$row['formula'] = is_array($formula) ? $formula : [];
|
|
unset($row['formula_json']);
|
|
}
|
|
unset($row);
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'virtual_points' => $rows,
|
|
'can_edit' => $canEditDesigner,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'virtual_point_save') {
|
|
if (!$canEditDesigner) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Speichern.'], 403);
|
|
}
|
|
|
|
if (!visuVirtualPointsTableReady($pdo)) {
|
|
visuJson([
|
|
'ok' => false,
|
|
'error' => 'Tabelle visu_virtual_points fehlt. Bitte SQL-Migration ausfuehren.',
|
|
], 500);
|
|
}
|
|
|
|
$pointId = (int)($_POST['point_id'] ?? 0);
|
|
$projectId = (int)($_POST['project_id'] ?? 0);
|
|
$name = trim((string)($_POST['name'] ?? ''));
|
|
$pointKey = trim((string)($_POST['point_key'] ?? ''));
|
|
$description = visuCleanString($_POST['description'] ?? '');
|
|
$unit = trim((string)($_POST['unit'] ?? ''));
|
|
$decimals = max(0, min(8, (int)($_POST['decimals'] ?? 2)));
|
|
$sortOrder = (int)($_POST['sort_order'] ?? 100);
|
|
$formulaRaw = (string)($_POST['formula_json'] ?? '[]');
|
|
$formula = json_decode($formulaRaw, true);
|
|
|
|
if ($projectId <= 0 || $name === '') {
|
|
visuJson(['ok' => false, 'error' => 'Projekt oder Name fehlt.'], 400);
|
|
}
|
|
|
|
if (!visuProjectVisible($pdo, $projectId, $userId, $canViewAll)) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt nicht sichtbar.'], 403);
|
|
}
|
|
|
|
if ($pointKey === '') {
|
|
$pointKey = visuSlug($name);
|
|
} else {
|
|
$pointKey = visuSlug($pointKey);
|
|
}
|
|
|
|
if ($pointKey === '') {
|
|
visuJson(['ok' => false, 'error' => 'Punkt-Key fehlt.'], 400);
|
|
}
|
|
|
|
if (!is_array($formula)) {
|
|
visuJson(['ok' => false, 'error' => 'Ungültige Formel.'], 400);
|
|
}
|
|
|
|
$formulaJson = json_encode($formula, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
try {
|
|
$check = $pdo->prepare("
|
|
SELECT id
|
|
FROM visu_virtual_points
|
|
WHERE project_id = ?
|
|
AND point_key = ?
|
|
AND id <> ?
|
|
LIMIT 1
|
|
");
|
|
$check->execute([$projectId, $pointKey, $pointId]);
|
|
|
|
if ($check->fetchColumn()) {
|
|
visuJson(['ok' => false, 'error' => 'Punkt-Key ist bereits vergeben.'], 400);
|
|
}
|
|
|
|
if ($pointId > 0) {
|
|
$stmt = $pdo->prepare("
|
|
UPDATE visu_virtual_points
|
|
SET
|
|
project_id = ?,
|
|
point_key = ?,
|
|
name = ?,
|
|
description = ?,
|
|
unit = ?,
|
|
decimals = ?,
|
|
sort_order = ?,
|
|
formula_json = ?,
|
|
is_active = 1,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
");
|
|
$stmt->execute([
|
|
$projectId,
|
|
$pointKey,
|
|
$name,
|
|
$description,
|
|
$unit,
|
|
$decimals,
|
|
$sortOrder,
|
|
$formulaJson,
|
|
$pointId,
|
|
]);
|
|
|
|
visuJson(['ok' => true, 'point_id' => $pointId]);
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO visu_virtual_points
|
|
(
|
|
project_id,
|
|
point_key,
|
|
name,
|
|
description,
|
|
unit,
|
|
decimals,
|
|
sort_order,
|
|
formula_json,
|
|
created_by,
|
|
is_active
|
|
)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
|
|
");
|
|
$stmt->execute([
|
|
$projectId,
|
|
$pointKey,
|
|
$name,
|
|
$description,
|
|
$unit,
|
|
$decimals,
|
|
$sortOrder,
|
|
$formulaJson,
|
|
$userId,
|
|
]);
|
|
|
|
visuJson(['ok' => true, 'point_id' => (int)$pdo->lastInsertId()]);
|
|
} catch (Throwable $e) {
|
|
visuJson(['ok' => false, 'error' => $e->getMessage()], 500);
|
|
}
|
|
}
|
|
|
|
if ($action === 'virtual_point_delete') {
|
|
if (!$canEditDesigner) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Loeschen.'], 403);
|
|
}
|
|
|
|
if (!visuVirtualPointsTableReady($pdo)) {
|
|
visuJson([
|
|
'ok' => false,
|
|
'error' => 'Tabelle visu_virtual_points fehlt. Bitte SQL-Migration ausfuehren.',
|
|
], 500);
|
|
}
|
|
|
|
$pointId = (int)($_POST['point_id'] ?? 0);
|
|
|
|
if ($pointId <= 0) {
|
|
visuJson(['ok' => false, 'error' => 'VDP fehlt.'], 400);
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
UPDATE visu_virtual_points
|
|
SET
|
|
is_active = 0,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
");
|
|
$stmt->execute([$pointId]);
|
|
|
|
visuJson(['ok' => true]);
|
|
}
|
|
|
|
if ($action === 'project_values') {
|
|
$projectId = (int)($_GET['project_id'] ?? 0);
|
|
|
|
if ($projectId <= 0) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt fehlt.'], 400);
|
|
}
|
|
|
|
if (!visuProjectVisible($pdo, $projectId, $userId, $canViewAll)) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt nicht sichtbar.'], 403);
|
|
}
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'values' => visuGetProjectPointsWithVirtual($pdo, $projectId, true),
|
|
'can_write_setpoints' => $canWriteSetpoints,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'trend_sources') {
|
|
$allowedDatabases = visuAllowedInfluxDatabases($pdo, $userId, $canViewAll);
|
|
$database = visuCleanString($_GET['database'] ?? '') ?: ($allowedDatabases[0] ?? '');
|
|
|
|
if (empty($allowedDatabases)) {
|
|
visuJson([
|
|
'ok' => true,
|
|
'databases' => [],
|
|
'selected_database' => '',
|
|
'points' => [],
|
|
]);
|
|
}
|
|
|
|
if (!in_array($database, $allowedDatabases, true)) {
|
|
visuJson(['ok' => false, 'error' => 'Keine Berechtigung fuer diese Influx-Datenbank.'], 403);
|
|
}
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'databases' => $allowedDatabases,
|
|
'selected_database' => $database,
|
|
'points' => visuInfluxListTrendPoints($database),
|
|
]);
|
|
}
|
|
|
|
if ($action === 'trend_data') {
|
|
$seriesRaw = (string)($_POST['series_json'] ?? $_GET['series_json'] ?? '[]');
|
|
$range = (string)($_POST['range'] ?? $_GET['range'] ?? '24h');
|
|
$interval = (string)($_POST['interval'] ?? $_GET['interval'] ?? 'auto');
|
|
$aggregate = (string)($_POST['aggregate'] ?? $_GET['aggregate'] ?? 'mean');
|
|
$start = $_POST['start'] ?? $_GET['start'] ?? null;
|
|
$end = $_POST['end'] ?? $_GET['end'] ?? null;
|
|
|
|
$series = json_decode($seriesRaw, true);
|
|
|
|
if (!is_array($series)) {
|
|
visuJson(['ok' => false, 'error' => 'Ungueltige Trend-Serien.'], 400);
|
|
}
|
|
|
|
$allowedDatabases = visuAllowedInfluxDatabases($pdo, $userId, $canViewAll);
|
|
$data = visuInfluxReadTrendSeries($series, $allowedDatabases, $range, $interval, $aggregate, $start, $end);
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'data' => $data,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'influx_latest_values') {
|
|
$seriesRaw = (string)($_POST['series_json'] ?? $_GET['series_json'] ?? '[]');
|
|
$series = json_decode($seriesRaw, true);
|
|
|
|
if (!is_array($series)) {
|
|
visuJson(['ok' => false, 'error' => 'Ungueltige Influx-Serien.'], 400);
|
|
}
|
|
|
|
$allowedDatabases = visuAllowedInfluxDatabases($pdo, $userId, $canViewAll);
|
|
$data = visuInfluxReadLatestValues($series, $allowedDatabases);
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'series' => $data,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'monitoring_selections') {
|
|
try {
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
id,
|
|
name,
|
|
database_name,
|
|
range_value,
|
|
interval_value,
|
|
aggregate_value,
|
|
delimiter_value,
|
|
points_json,
|
|
updated_at
|
|
FROM visu_monitoring_selections
|
|
WHERE user_id = ?
|
|
AND is_active = 1
|
|
ORDER BY updated_at DESC, name ASC
|
|
");
|
|
$stmt->execute([$userId]);
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (Throwable $e) {
|
|
visuJson(['ok' => false, 'error' => 'Tabelle visu_monitoring_selections fehlt oder ist nicht erreichbar.'], 500);
|
|
}
|
|
|
|
foreach ($rows as &$row) {
|
|
$decoded = json_decode((string)($row['points_json'] ?? '[]'), true);
|
|
$row['points'] = is_array($decoded) ? $decoded : [];
|
|
$row['csv_delimiter'] = (string)($row['delimiter_value'] ?? ';');
|
|
unset($row['points_json']);
|
|
}
|
|
unset($row);
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'selections' => $rows,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'monitoring_selection_save') {
|
|
$selectionId = (int)($_POST['selection_id'] ?? 0);
|
|
$name = trim((string)($_POST['name'] ?? ''));
|
|
$database = visuCleanString($_POST['database_name'] ?? $_POST['database'] ?? '');
|
|
$range = (string)($_POST['range_value'] ?? $_POST['range'] ?? '24h');
|
|
$interval = (string)($_POST['interval_value'] ?? $_POST['interval'] ?? 'auto');
|
|
$aggregate = visuTrendAggregate((string)($_POST['aggregate_value'] ?? $_POST['aggregate'] ?? 'last'));
|
|
$delimiter = (string)($_POST['csv_delimiter'] ?? $_POST['delimiter_value'] ?? $_POST['delimiter'] ?? ';');
|
|
$pointsRaw = (string)($_POST['points_json'] ?? '[]');
|
|
$points = json_decode($pointsRaw, true);
|
|
|
|
if ($name === '') {
|
|
visuJson(['ok' => false, 'error' => 'Name fehlt.'], 400);
|
|
}
|
|
|
|
if (!is_array($points)) {
|
|
visuJson(['ok' => false, 'error' => 'Ungueltige Datenpunktliste.'], 400);
|
|
}
|
|
|
|
$allowedDatabases = visuAllowedInfluxDatabases($pdo, $userId, $canViewAll);
|
|
if ($database && !in_array($database, $allowedDatabases, true)) {
|
|
visuJson(['ok' => false, 'error' => 'Keine Berechtigung fuer diese Influx-Datenbank.'], 403);
|
|
}
|
|
|
|
$range = in_array($range, ['15m', '1h', '6h', '12h', '24h', '7d', '30d', '90d', '180d'], true) ? $range : '24h';
|
|
$interval = in_array($interval, ['auto', '10s', '30s', '1m', '5m', '10m', '15m', '30m', '1h', '6h', '12h', '1d'], true) ? $interval : 'auto';
|
|
$delimiter = visuCsvDelimiter($delimiter);
|
|
$pointsJson = json_encode(array_values($points), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
try {
|
|
if ($selectionId > 0) {
|
|
$stmt = $pdo->prepare("
|
|
UPDATE visu_monitoring_selections
|
|
SET
|
|
name = ?,
|
|
database_name = ?,
|
|
range_value = ?,
|
|
interval_value = ?,
|
|
aggregate_value = ?,
|
|
delimiter_value = ?,
|
|
points_json = ?,
|
|
is_active = 1
|
|
WHERE id = ?
|
|
AND user_id = ?
|
|
");
|
|
$stmt->execute([$name, $database, $range, $interval, $aggregate, $delimiter, $pointsJson, $selectionId, $userId]);
|
|
} else {
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO visu_monitoring_selections
|
|
(user_id, name, database_name, range_value, interval_value, aggregate_value, delimiter_value, points_json, is_active)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, ?, ?, 1)
|
|
ON DUPLICATE KEY UPDATE
|
|
database_name = VALUES(database_name),
|
|
range_value = VALUES(range_value),
|
|
interval_value = VALUES(interval_value),
|
|
aggregate_value = VALUES(aggregate_value),
|
|
delimiter_value = VALUES(delimiter_value),
|
|
points_json = VALUES(points_json),
|
|
is_active = 1
|
|
");
|
|
$stmt->execute([$userId, $name, $database, $range, $interval, $aggregate, $delimiter, $pointsJson]);
|
|
$selectionId = (int)$pdo->lastInsertId();
|
|
}
|
|
|
|
if ($selectionId <= 0) {
|
|
$stmt = $pdo->prepare("
|
|
SELECT id
|
|
FROM visu_monitoring_selections
|
|
WHERE user_id = ?
|
|
AND name = ?
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([$userId, $name]);
|
|
$selectionId = (int)($stmt->fetchColumn() ?: 0);
|
|
}
|
|
} catch (Throwable $e) {
|
|
visuJson(['ok' => false, 'error' => 'Selection konnte nicht gespeichert werden. Ist die Tabelle angelegt?'], 500);
|
|
}
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'selection_id' => $selectionId,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'monitoring_selection_delete') {
|
|
$selectionId = (int)($_POST['selection_id'] ?? 0);
|
|
|
|
if ($selectionId <= 0) {
|
|
visuJson(['ok' => false, 'error' => 'Selection fehlt.'], 400);
|
|
}
|
|
|
|
try {
|
|
$stmt = $pdo->prepare("
|
|
UPDATE visu_monitoring_selections
|
|
SET is_active = 0
|
|
WHERE id = ?
|
|
AND user_id = ?
|
|
");
|
|
$stmt->execute([$selectionId, $userId]);
|
|
} catch (Throwable $e) {
|
|
visuJson(['ok' => false, 'error' => 'Selection konnte nicht geloescht werden.'], 500);
|
|
}
|
|
|
|
visuJson(['ok' => true]);
|
|
}
|
|
|
|
if ($action === 'csv_export_jobs') {
|
|
$projectId = (int)($_GET['project_id'] ?? 0);
|
|
|
|
if ($projectId <= 0) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt fehlt.'], 400);
|
|
}
|
|
|
|
if (!visuProjectVisible($pdo, $projectId, $userId, $canViewAll)) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt nicht sichtbar.'], 403);
|
|
}
|
|
|
|
if (!visuCsvExportJobsTableReady($pdo)) {
|
|
visuJson(['ok' => false, 'error' => 'Tabelle visu_csv_export_jobs fehlt. Bitte SQL-Migration ausfuehren.'], 500);
|
|
}
|
|
|
|
try {
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
j.*,
|
|
s.name AS selection_name
|
|
FROM visu_csv_export_jobs j
|
|
LEFT JOIN visu_monitoring_selections s
|
|
ON s.id = j.selection_id
|
|
AND s.user_id = ?
|
|
AND s.is_active = 1
|
|
WHERE j.project_id = ?
|
|
ORDER BY j.updated_at DESC, j.name ASC
|
|
");
|
|
$stmt->execute([$userId, $projectId]);
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (Throwable $e) {
|
|
visuJson(['ok' => false, 'error' => 'Exportjobs konnten nicht geladen werden.'], 500);
|
|
}
|
|
|
|
foreach ($rows as &$row) {
|
|
$row['enabled'] = (int)($row['enabled'] ?? 0);
|
|
$row['interval_minutes'] = (int)($row['interval_minutes'] ?? 60);
|
|
$row['month_day'] = (int)($row['month_day'] ?? 1);
|
|
$row['retention_days'] = (int)($row['retention_days'] ?? 30);
|
|
$row['project_id'] = (int)($row['project_id'] ?? 0);
|
|
$row['selection_id'] = (int)($row['selection_id'] ?? 0);
|
|
}
|
|
unset($row);
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'jobs' => $rows,
|
|
'can_edit' => $canEditDesigner,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'csv_export_job_save') {
|
|
if (!$canEditDesigner) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Speichern.'], 403);
|
|
}
|
|
|
|
if (!visuCsvExportJobsTableReady($pdo)) {
|
|
visuJson(['ok' => false, 'error' => 'Tabelle visu_csv_export_jobs fehlt. Bitte SQL-Migration ausfuehren.'], 500);
|
|
}
|
|
|
|
$jobId = (int)($_POST['job_id'] ?? 0);
|
|
$projectId = (int)($_POST['project_id'] ?? 0);
|
|
$selectionId = (int)($_POST['selection_id'] ?? 0);
|
|
$name = trim((string)($_POST['name'] ?? ''));
|
|
$exportKind = visuCsvExportKind((string)($_POST['export_kind'] ?? 'trend_csv'));
|
|
$csvDelimiter = visuCsvDelimiter($_POST['csv_delimiter'] ?? ';');
|
|
$enabled = !empty($_POST['enabled']) ? 1 : 0;
|
|
$scheduleKind = visuCsvScheduleKind((string)($_POST['schedule_kind'] ?? 'daily'));
|
|
$intervalMinutes = visuCsvIntervalMinutes($_POST['interval_minutes'] ?? 60);
|
|
$timeOfDay = visuCsvTimeOfDay((string)($_POST['time_of_day'] ?? '00:00'));
|
|
$weekdays = visuCsvWeekdays((string)($_POST['weekdays'] ?? ''));
|
|
$monthDay = visuCsvMonthDay($_POST['month_day'] ?? 1);
|
|
$targetDir = visuCsvTargetDir((string)($_POST['target_dir'] ?? 'csv_exports'));
|
|
$filenamePattern = visuCsvFilenamePattern((string)($_POST['filename_pattern'] ?? ''));
|
|
$retentionDays = visuCsvRetentionDays($_POST['retention_days'] ?? 30);
|
|
|
|
if ($projectId <= 0 || $selectionId <= 0 || $name === '') {
|
|
visuJson(['ok' => false, 'error' => 'Projekt, Selection oder Name fehlt.'], 400);
|
|
}
|
|
|
|
if (!visuProjectVisible($pdo, $projectId, $userId, $canViewAll)) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt nicht sichtbar.'], 403);
|
|
}
|
|
|
|
$selectionStmt = $pdo->prepare("
|
|
SELECT id, name
|
|
FROM visu_monitoring_selections
|
|
WHERE id = ?
|
|
AND user_id = ?
|
|
AND is_active = 1
|
|
LIMIT 1
|
|
");
|
|
$selectionStmt->execute([$selectionId, $userId]);
|
|
$selectionRow = $selectionStmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$selectionRow) {
|
|
visuJson(['ok' => false, 'error' => 'Selection nicht gefunden oder nicht erlaubt.'], 404);
|
|
}
|
|
|
|
if ($scheduleKind === 'weekly' && $weekdays === '') {
|
|
$weekdays = (string)(new DateTimeImmutable('now'))->format('N');
|
|
}
|
|
|
|
$nextRunAt = $enabled
|
|
? visuCsvNextRunAt($scheduleKind, $intervalMinutes, $timeOfDay, $weekdays, $monthDay)
|
|
: null;
|
|
|
|
try {
|
|
if ($jobId > 0) {
|
|
$stmt = $pdo->prepare("
|
|
UPDATE visu_csv_export_jobs
|
|
SET
|
|
project_id = ?,
|
|
selection_id = ?,
|
|
name = ?,
|
|
export_kind = ?,
|
|
csv_delimiter = ?,
|
|
enabled = ?,
|
|
schedule_kind = ?,
|
|
interval_minutes = ?,
|
|
time_of_day = ?,
|
|
weekdays = ?,
|
|
month_day = ?,
|
|
target_dir = ?,
|
|
filename_pattern = ?,
|
|
retention_days = ?,
|
|
next_run_at = ?
|
|
WHERE id = ?
|
|
");
|
|
$stmt->execute([
|
|
$projectId,
|
|
$selectionId,
|
|
$name,
|
|
$exportKind,
|
|
$csvDelimiter,
|
|
$enabled,
|
|
$scheduleKind,
|
|
$intervalMinutes,
|
|
$timeOfDay,
|
|
$weekdays,
|
|
$monthDay,
|
|
$targetDir,
|
|
$filenamePattern,
|
|
$retentionDays,
|
|
$nextRunAt,
|
|
$jobId,
|
|
]);
|
|
} else {
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO visu_csv_export_jobs (
|
|
project_id,
|
|
selection_id,
|
|
name,
|
|
export_kind,
|
|
csv_delimiter,
|
|
enabled,
|
|
schedule_kind,
|
|
interval_minutes,
|
|
time_of_day,
|
|
weekdays,
|
|
month_day,
|
|
target_dir,
|
|
filename_pattern,
|
|
retention_days,
|
|
next_run_at,
|
|
created_by
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
");
|
|
$stmt->execute([
|
|
$projectId,
|
|
$selectionId,
|
|
$name,
|
|
$exportKind,
|
|
$csvDelimiter,
|
|
$enabled,
|
|
$scheduleKind,
|
|
$intervalMinutes,
|
|
$timeOfDay,
|
|
$weekdays,
|
|
$monthDay,
|
|
$targetDir,
|
|
$filenamePattern,
|
|
$retentionDays,
|
|
$nextRunAt,
|
|
$userId,
|
|
]);
|
|
$jobId = (int)$pdo->lastInsertId();
|
|
}
|
|
} catch (Throwable $e) {
|
|
visuJson(['ok' => false, 'error' => 'Exportjob konnte nicht gespeichert werden.'], 500);
|
|
}
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'job_id' => $jobId,
|
|
'next_run_at' => $nextRunAt,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'csv_export_job_delete') {
|
|
if (!$canEditDesigner) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Loeschen.'], 403);
|
|
}
|
|
|
|
if (!visuCsvExportJobsTableReady($pdo)) {
|
|
visuJson(['ok' => false, 'error' => 'Tabelle visu_csv_export_jobs fehlt. Bitte SQL-Migration ausfuehren.'], 500);
|
|
}
|
|
|
|
$jobId = (int)($_POST['job_id'] ?? 0);
|
|
if ($jobId <= 0) {
|
|
visuJson(['ok' => false, 'error' => 'Job fehlt.'], 400);
|
|
}
|
|
|
|
try {
|
|
$stmt = $pdo->prepare("DELETE FROM visu_csv_export_jobs WHERE id = ?");
|
|
$stmt->execute([$jobId]);
|
|
} catch (Throwable $e) {
|
|
visuJson(['ok' => false, 'error' => 'Exportjob konnte nicht geloescht werden.'], 500);
|
|
}
|
|
|
|
visuJson(['ok' => true]);
|
|
}
|
|
|
|
if ($action === 'csv_export_files') {
|
|
$projectId = (int)($_GET['project_id'] ?? 0);
|
|
$jobId = (int)($_GET['job_id'] ?? 0);
|
|
|
|
if ($projectId <= 0) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt fehlt.'], 400);
|
|
}
|
|
|
|
if (!visuProjectVisible($pdo, $projectId, $userId, $canViewAll)) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt nicht sichtbar.'], 403);
|
|
}
|
|
|
|
if (!visuCsvExportFilesTableReady($pdo)) {
|
|
visuJson(['ok' => false, 'error' => 'Tabelle visu_csv_export_files fehlt. Bitte SQL-Migration ausfuehren.'], 500);
|
|
}
|
|
|
|
try {
|
|
$sql = "
|
|
SELECT *
|
|
FROM visu_csv_export_files
|
|
WHERE project_id = ?
|
|
";
|
|
$params = [$projectId];
|
|
|
|
if ($jobId > 0) {
|
|
$sql .= " AND job_id = ?";
|
|
$params[] = $jobId;
|
|
}
|
|
|
|
$sql .= " ORDER BY created_at DESC, id DESC LIMIT 500";
|
|
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (Throwable $e) {
|
|
visuJson(['ok' => false, 'error' => 'Exportdateien konnten nicht geladen werden.'], 500);
|
|
}
|
|
|
|
foreach ($rows as &$row) {
|
|
$row['job_id'] = (int)($row['job_id'] ?? 0);
|
|
$row['project_id'] = (int)($row['project_id'] ?? 0);
|
|
$row['selection_id'] = (int)($row['selection_id'] ?? 0);
|
|
$row['file_size'] = (int)($row['file_size'] ?? 0);
|
|
$row['row_count'] = (int)($row['row_count'] ?? 0);
|
|
}
|
|
unset($row);
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'files' => $rows,
|
|
'can_edit' => $canEditDesigner,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'csv_export_file_delete') {
|
|
if (!$canEditDesigner) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Loeschen.'], 403);
|
|
}
|
|
|
|
if (!visuCsvExportFilesTableReady($pdo)) {
|
|
visuJson(['ok' => false, 'error' => 'Tabelle visu_csv_export_files fehlt. Bitte SQL-Migration ausfuehren.'], 500);
|
|
}
|
|
|
|
$fileId = (int)($_POST['file_id'] ?? 0);
|
|
if ($fileId <= 0) {
|
|
visuJson(['ok' => false, 'error' => 'Datei fehlt.'], 400);
|
|
}
|
|
|
|
try {
|
|
$stmt = $pdo->prepare("SELECT file_path FROM visu_csv_export_files WHERE id = ? LIMIT 1");
|
|
$stmt->execute([$fileId]);
|
|
$filePath = (string)($stmt->fetchColumn() ?: '');
|
|
|
|
if ($filePath !== '') {
|
|
$absolutePath = visuCsvExportAbsolutePath($filePath);
|
|
if (is_file($absolutePath)) {
|
|
@unlink($absolutePath);
|
|
}
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
UPDATE visu_csv_export_files
|
|
SET status = 'deleted'
|
|
WHERE id = ?
|
|
");
|
|
$stmt->execute([$fileId]);
|
|
} catch (Throwable $e) {
|
|
visuJson(['ok' => false, 'error' => 'Exportdatei konnte nicht markiert werden.'], 500);
|
|
}
|
|
|
|
visuJson(['ok' => true]);
|
|
}
|
|
|
|
if ($action === 'csv_export_file_download') {
|
|
$fileId = (int)($_GET['file_id'] ?? 0);
|
|
if ($fileId <= 0) {
|
|
visuJson(['ok' => false, 'error' => 'Datei fehlt.'], 400);
|
|
}
|
|
|
|
if (!visuCsvExportFilesTableReady($pdo)) {
|
|
visuJson(['ok' => false, 'error' => 'Tabelle visu_csv_export_files fehlt. Bitte SQL-Migration ausfuehren.'], 500);
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT id, project_id, file_name, file_path, status
|
|
FROM visu_csv_export_files
|
|
WHERE id = ?
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([$fileId]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$row) {
|
|
visuJson(['ok' => false, 'error' => 'Datei nicht gefunden.'], 404);
|
|
}
|
|
|
|
$projectId = (int)($row['project_id'] ?? 0);
|
|
if ($projectId <= 0 || !visuProjectVisible($pdo, $projectId, $userId, $canViewAll)) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Zugriff auf diese Exportdatei.'], 403);
|
|
}
|
|
|
|
if (($row['status'] ?? '') !== 'success') {
|
|
visuJson(['ok' => false, 'error' => 'Datei ist noch nicht verfuegbar.'], 409);
|
|
}
|
|
|
|
$absolutePath = visuCsvExportAbsolutePath((string)($row['file_path'] ?? ''));
|
|
if (!is_file($absolutePath)) {
|
|
visuJson(['ok' => false, 'error' => 'Datei fehlt im Export-Speicher.'], 404);
|
|
}
|
|
|
|
header_remove('Content-Type');
|
|
header('Content-Type: text/csv; charset=utf-8');
|
|
header('Content-Length: ' . (string)filesize($absolutePath));
|
|
header('Content-Disposition: attachment; filename="' . str_replace('"', '', (string)($row['file_name'] ?? 'export.csv')) . '"');
|
|
readfile($absolutePath);
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'pages') {
|
|
$params = [];
|
|
$visSql = visuVisibilitySql($userId, $canViewAll, 'vp.project_id', $params, 'pages');
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
vp.id,
|
|
vp.project_id,
|
|
vp.name,
|
|
vp.slug,
|
|
vp.description,
|
|
vp.width,
|
|
vp.height,
|
|
vp.background_color,
|
|
vp.background_image,
|
|
vp.grid_enabled,
|
|
vp.grid_size,
|
|
vp.snap_enabled,
|
|
vp.is_active,
|
|
vp.created_at,
|
|
vp.updated_at,
|
|
p.project_number,
|
|
p.year,
|
|
p.name AS project_name,
|
|
pc.company_name AS customer_name
|
|
FROM visu_pages vp
|
|
INNER JOIN projects p ON p.id = vp.project_id
|
|
LEFT JOIN project_customers pc ON pc.id = p.customer_id
|
|
WHERE vp.is_active = 1
|
|
AND {$visSql}
|
|
ORDER BY p.year DESC, p.project_number ASC, vp.name ASC
|
|
");
|
|
$stmt->execute($params);
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'pages' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
|
'can_edit' => $canEditDesigner,
|
|
'can_design' => $canViewDesigner,
|
|
'can_manage_assets' => $canManageAssets,
|
|
'can_edit_assets' => $canEditAssets,
|
|
'can_delete_assets' => $canDeleteAssets,
|
|
'can_write_setpoints' => $canWriteSetpoints,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'dashboard_widgets') {
|
|
$params = [':dashboard_user_id' => $userId];
|
|
$visSql = visuVisibilitySql($userId, $canViewAll, 'dw.project_id', $params, 'dashboard_widgets');
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
dw.*,
|
|
p.project_number,
|
|
p.year,
|
|
p.name AS project_name,
|
|
pc.company_name AS customer_name
|
|
FROM visu_dashboard_widgets dw
|
|
LEFT JOIN projects p ON p.id = dw.project_id
|
|
LEFT JOIN project_customers pc ON pc.id = p.customer_id
|
|
WHERE dw.user_id = :dashboard_user_id
|
|
AND dw.is_active = 1
|
|
AND (dw.project_id IS NULL OR {$visSql})
|
|
ORDER BY dw.sort_order ASC, dw.grid_y ASC, dw.grid_x ASC, dw.id ASC
|
|
");
|
|
$stmt->execute($params);
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'widgets' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
|
]);
|
|
}
|
|
|
|
if ($action === 'dashboard_widget_save') {
|
|
$widgetId = (int)($_POST['widget_id'] ?? 0);
|
|
$widgetType = trim((string)($_POST['widget_type'] ?? ''));
|
|
$title = visuCleanString($_POST['title'] ?? '');
|
|
$projectId = (int)($_POST['project_id'] ?? 0);
|
|
$gridX = max(0, (int)($_POST['grid_x'] ?? 0));
|
|
$gridY = max(0, (int)($_POST['grid_y'] ?? 0));
|
|
$gridW = max(1, min(4, (int)($_POST['grid_w'] ?? 1)));
|
|
$gridH = max(1, min(4, (int)($_POST['grid_h'] ?? 1)));
|
|
$sortOrder = (int)($_POST['sort_order'] ?? 100);
|
|
$configJson = trim((string)($_POST['config_json'] ?? '{}'));
|
|
|
|
if (!in_array($widgetType, ['simple_value', 'value_table', 'scale_bar', 'dial_gauge'], true)) {
|
|
visuJson(['ok' => false, 'error' => 'Ungültiger Widget-Typ.'], 400);
|
|
}
|
|
|
|
$config = json_decode($configJson, true);
|
|
if (!is_array($config)) {
|
|
visuJson(['ok' => false, 'error' => 'Ungültige Widget-Konfiguration.'], 400);
|
|
}
|
|
|
|
if ($projectId > 0 && !visuProjectVisible($pdo, $projectId, $userId, $canViewAll)) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt nicht sichtbar.'], 403);
|
|
}
|
|
|
|
$projectValue = $projectId > 0 ? $projectId : null;
|
|
$configText = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
if ($widgetId > 0) {
|
|
$stmt = $pdo->prepare("
|
|
UPDATE visu_dashboard_widgets
|
|
SET
|
|
widget_type = ?,
|
|
title = ?,
|
|
project_id = ?,
|
|
grid_x = ?,
|
|
grid_y = ?,
|
|
grid_w = ?,
|
|
grid_h = ?,
|
|
sort_order = ?,
|
|
config_json = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
AND user_id = ?
|
|
");
|
|
$stmt->execute([
|
|
$widgetType,
|
|
$title,
|
|
$projectValue,
|
|
$gridX,
|
|
$gridY,
|
|
$gridW,
|
|
$gridH,
|
|
$sortOrder,
|
|
$configText,
|
|
$widgetId,
|
|
$userId,
|
|
]);
|
|
|
|
if ($stmt->rowCount() === 0) {
|
|
$chk = $pdo->prepare("SELECT 1 FROM visu_dashboard_widgets WHERE id = ? AND user_id = ? LIMIT 1");
|
|
$chk->execute([$widgetId, $userId]);
|
|
|
|
if (!$chk->fetchColumn()) {
|
|
visuJson(['ok' => false, 'error' => 'Widget nicht gefunden.'], 404);
|
|
}
|
|
}
|
|
|
|
visuJson(['ok' => true, 'widget_id' => $widgetId]);
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO visu_dashboard_widgets
|
|
(
|
|
user_id,
|
|
project_id,
|
|
widget_type,
|
|
title,
|
|
grid_x,
|
|
grid_y,
|
|
grid_w,
|
|
grid_h,
|
|
sort_order,
|
|
config_json,
|
|
is_active
|
|
)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
|
|
");
|
|
$stmt->execute([
|
|
$userId,
|
|
$projectValue,
|
|
$widgetType,
|
|
$title,
|
|
$gridX,
|
|
$gridY,
|
|
$gridW,
|
|
$gridH,
|
|
$sortOrder,
|
|
$configText,
|
|
]);
|
|
|
|
visuJson(['ok' => true, 'widget_id' => (int)$pdo->lastInsertId()]);
|
|
}
|
|
|
|
if ($action === 'dashboard_widget_delete') {
|
|
$widgetId = (int)($_POST['widget_id'] ?? 0);
|
|
|
|
if ($widgetId <= 0) {
|
|
visuJson(['ok' => false, 'error' => 'Widget fehlt.'], 400);
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
UPDATE visu_dashboard_widgets
|
|
SET
|
|
is_active = 0,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
AND user_id = ?
|
|
");
|
|
$stmt->execute([$widgetId, $userId]);
|
|
|
|
if ($stmt->rowCount() === 0) {
|
|
visuJson(['ok' => false, 'error' => 'Widget nicht gefunden.'], 404);
|
|
}
|
|
|
|
visuJson(['ok' => true]);
|
|
}
|
|
|
|
if ($action === 'templates') {
|
|
if (!$canViewDesigner) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Zugriff auf Templates.'], 403);
|
|
}
|
|
|
|
$stmt = $pdo->query("
|
|
SELECT
|
|
id,
|
|
name,
|
|
description,
|
|
width,
|
|
height,
|
|
placeholders_json,
|
|
updated_at
|
|
FROM visu_templates
|
|
WHERE is_active = 1
|
|
ORDER BY updated_at DESC, name ASC
|
|
");
|
|
|
|
$templates = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
foreach ($templates as &$template) {
|
|
$placeholders = json_decode((string)($template['placeholders_json'] ?? '[]'), true);
|
|
$template['placeholder_count'] = is_array($placeholders) ? count($placeholders) : 0;
|
|
unset($template['placeholders_json']);
|
|
}
|
|
unset($template);
|
|
|
|
visuJson(['ok' => true, 'templates' => $templates, 'can_edit' => $canEditDesigner]);
|
|
}
|
|
|
|
if ($action === 'template') {
|
|
if (!$canViewDesigner) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Zugriff auf Templates.'], 403);
|
|
}
|
|
|
|
$templateId = (int)($_GET['id'] ?? 0);
|
|
$stmt = $pdo->prepare("SELECT * FROM visu_templates WHERE id = ? AND is_active = 1 LIMIT 1");
|
|
$stmt->execute([$templateId]);
|
|
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$template) {
|
|
visuJson(['ok' => false, 'error' => 'Template nicht gefunden.'], 404);
|
|
}
|
|
|
|
$placeholders = json_decode((string)($template['placeholders_json'] ?? '[]'), true);
|
|
$elements = json_decode((string)($template['elements_json'] ?? '[]'), true);
|
|
|
|
unset($template['placeholders_json'], $template['elements_json']);
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'template' => $template,
|
|
'placeholders' => is_array($placeholders) ? $placeholders : [],
|
|
'elements' => is_array($elements) ? $elements : [],
|
|
'can_edit' => $canEditDesigner,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'template_save' || $action === 'template_settings_save') {
|
|
if (!$canEditDesigner) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Speichern.'], 403);
|
|
}
|
|
|
|
$templateId = (int)($_POST['template_id'] ?? 0);
|
|
$name = visuCleanString($_POST['name'] ?? null) ?: 'Template';
|
|
$description = visuCleanString($_POST['description'] ?? null);
|
|
$width = max(320, (int)($_POST['width'] ?? 1920));
|
|
$height = max(240, (int)($_POST['height'] ?? 1080));
|
|
$backgroundColor = visuCleanString($_POST['background_color'] ?? null) ?: '#0f172a';
|
|
$gridEnabled = !empty($_POST['grid_enabled']) ? 1 : 0;
|
|
$snapEnabled = !empty($_POST['snap_enabled']) ? 1 : 0;
|
|
$gridSize = max(2, (int)($_POST['grid_size'] ?? 40));
|
|
$placeholdersRaw = (string)($_POST['placeholders_json'] ?? '[]');
|
|
$elementsRaw = (string)($_POST['elements_json'] ?? '');
|
|
|
|
$placeholders = json_decode($placeholdersRaw, true);
|
|
if (!is_array($placeholders)) {
|
|
visuJson(['ok' => false, 'error' => 'Ungültige Platzhalter.'], 400);
|
|
}
|
|
|
|
$placeholdersJson = json_encode($placeholders, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
if ($templateId > 0) {
|
|
$stmt = $pdo->prepare("SELECT elements_json, background_image FROM visu_templates WHERE id = ? AND is_active = 1 LIMIT 1");
|
|
$stmt->execute([$templateId]);
|
|
$oldTemplate = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$oldTemplate) {
|
|
visuJson(['ok' => false, 'error' => 'Template nicht gefunden.'], 404);
|
|
}
|
|
|
|
$backgroundImage = (string)($oldTemplate['background_image'] ?? '');
|
|
$uploaded = visuSaveUploadedImage('background_image', VISU_BG_UPLOAD_DIR, VISU_BG_UPLOAD_URL);
|
|
|
|
if ($uploaded) {
|
|
visuDeleteUploadedFile($backgroundImage, VISU_BG_UPLOAD_URL, VISU_BG_UPLOAD_DIR);
|
|
$backgroundImage = $uploaded;
|
|
}
|
|
|
|
if (!empty($_POST['remove_background_image'])) {
|
|
visuDeleteUploadedFile($backgroundImage, VISU_BG_UPLOAD_URL, VISU_BG_UPLOAD_DIR);
|
|
$backgroundImage = null;
|
|
}
|
|
|
|
if ($elementsRaw !== '') {
|
|
$elements = json_decode($elementsRaw, true);
|
|
if (!is_array($elements)) {
|
|
visuJson(['ok' => false, 'error' => 'Ungültige Elemente.'], 400);
|
|
}
|
|
$elementsJson = json_encode($elements, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
} else {
|
|
$elementsJson = (string)($oldTemplate['elements_json'] ?? '[]');
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
UPDATE visu_templates
|
|
SET name = ?,
|
|
description = ?,
|
|
width = ?,
|
|
height = ?,
|
|
background_color = ?,
|
|
background_image = ?,
|
|
grid_enabled = ?,
|
|
snap_enabled = ?,
|
|
grid_size = ?,
|
|
placeholders_json = ?,
|
|
elements_json = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
");
|
|
$stmt->execute([
|
|
$name,
|
|
$description,
|
|
$width,
|
|
$height,
|
|
$backgroundColor,
|
|
$backgroundImage,
|
|
$gridEnabled,
|
|
$snapEnabled,
|
|
$gridSize,
|
|
$placeholdersJson,
|
|
$elementsJson,
|
|
$templateId,
|
|
]);
|
|
} else {
|
|
$elements = $elementsRaw !== '' ? json_decode($elementsRaw, true) : [];
|
|
if (!is_array($elements)) {
|
|
visuJson(['ok' => false, 'error' => 'Ungültige Elemente.'], 400);
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO visu_templates
|
|
(user_id, name, description, width, height, background_color, background_image, grid_enabled, snap_enabled, grid_size, placeholders_json, elements_json)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
");
|
|
$stmt->execute([
|
|
$userId,
|
|
$name,
|
|
$description,
|
|
$width,
|
|
$height,
|
|
$backgroundColor,
|
|
visuSaveUploadedImage('background_image', VISU_BG_UPLOAD_DIR, VISU_BG_UPLOAD_URL) ?: null,
|
|
$gridEnabled,
|
|
$snapEnabled,
|
|
$gridSize,
|
|
$placeholdersJson,
|
|
json_encode($elements, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
]);
|
|
$templateId = (int)$pdo->lastInsertId();
|
|
}
|
|
|
|
visuJson(['ok' => true, 'template_id' => $templateId]);
|
|
}
|
|
|
|
if ($action === 'template_delete') {
|
|
if (!$canEditDesigner) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Löschen.'], 403);
|
|
}
|
|
|
|
$templateId = (int)($_POST['template_id'] ?? 0);
|
|
$stmt = $pdo->prepare("UPDATE visu_templates SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
|
|
$stmt->execute([$templateId]);
|
|
visuJson(['ok' => true]);
|
|
}
|
|
|
|
if ($action === 'template_import') {
|
|
if (!$canEditDesigner) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Importieren.'], 403);
|
|
}
|
|
|
|
$payload = json_decode((string)($_POST['import_json'] ?? ''), true);
|
|
if (!is_array($payload)) {
|
|
visuJson(['ok' => false, 'error' => 'Ungültige Import-Datei.'], 400);
|
|
}
|
|
|
|
$template = is_array($payload['template'] ?? null) ? $payload['template'] : [];
|
|
$placeholders = is_array($payload['placeholders'] ?? null) ? $payload['placeholders'] : [];
|
|
$elements = is_array($payload['elements'] ?? null) ? $payload['elements'] : [];
|
|
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO visu_templates
|
|
(user_id, name, description, width, height, background_color, background_image, grid_enabled, snap_enabled, grid_size, placeholders_json, elements_json)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
");
|
|
$stmt->execute([
|
|
$userId,
|
|
visuCleanString(($template['name'] ?? 'Importiertes Template') . ' Import') ?: 'Importiertes Template',
|
|
visuCleanString($template['description'] ?? null),
|
|
max(320, (int)($template['width'] ?? 1920)),
|
|
max(240, (int)($template['height'] ?? 1080)),
|
|
visuCleanString($template['background_color'] ?? null) ?: '#0f172a',
|
|
visuCleanString($template['background_image'] ?? null),
|
|
!empty($template['grid_enabled']) ? 1 : 0,
|
|
!empty($template['snap_enabled']) ? 1 : 0,
|
|
max(2, (int)($template['grid_size'] ?? 40)),
|
|
json_encode($placeholders, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
json_encode($elements, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
]);
|
|
|
|
visuJson(['ok' => true, 'template_id' => (int)$pdo->lastInsertId()]);
|
|
}
|
|
|
|
if ($action === 'page') {
|
|
$pageId = (int)($_GET['id'] ?? 0);
|
|
$page = visuPageVisible($pdo, $pageId, $userId, $canViewAll);
|
|
|
|
if (!$page) {
|
|
visuJson(['ok' => false, 'error' => 'Visualisierung nicht gefunden.'], 404);
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
ve.*,
|
|
va.name AS asset_name,
|
|
va.asset_type
|
|
FROM visu_elements ve
|
|
LEFT JOIN visu_assets va ON va.id = ve.asset_id
|
|
WHERE ve.page_id = ?
|
|
ORDER BY ve.z_index ASC, ve.id ASC
|
|
");
|
|
$stmt->execute([$pageId]);
|
|
$elements = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$elementIds = array_map(fn($e) => (int)$e['id'], $elements);
|
|
$assetIds = array_map(fn($e) => (int)($e['asset_id'] ?? 0), $elements);
|
|
|
|
$statesByAsset = visuLoadAssetStatesForAssets($pdo, $assetIds);
|
|
$rulesByElement = [];
|
|
|
|
if (!empty($elementIds)) {
|
|
$ph = implode(',', array_fill(0, count($elementIds), '?'));
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
r.*,
|
|
s.state_key,
|
|
s.label AS state_label,
|
|
s.image_file
|
|
FROM visu_element_state_rules r
|
|
INNER JOIN visu_asset_states s ON s.id = r.asset_state_id
|
|
WHERE r.element_id IN ($ph)
|
|
ORDER BY r.element_id ASC, r.priority ASC, r.id ASC
|
|
");
|
|
$stmt->execute($elementIds);
|
|
|
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
$eid = (int)$row['element_id'];
|
|
$rulesByElement[$eid][] = $row;
|
|
}
|
|
}
|
|
|
|
foreach ($elements as &$el) {
|
|
$eid = (int)$el['id'];
|
|
$aid = (int)($el['asset_id'] ?? 0);
|
|
$el['rules'] = $rulesByElement[$eid] ?? [];
|
|
$el['asset_states'] = $statesByAsset[$aid] ?? [];
|
|
}
|
|
unset($el);
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'page' => $page,
|
|
'elements' => $elements,
|
|
'can_edit' => $canEditDesigner,
|
|
'can_design' => $canViewDesigner,
|
|
'can_manage_assets' => $canManageAssets,
|
|
'can_edit_assets' => $canEditAssets,
|
|
'can_delete_assets' => $canDeleteAssets,
|
|
'can_write_setpoints' => $canWriteSetpoints,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'page_create') {
|
|
if (!$canEditDesigner) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Erstellen.'], 403);
|
|
}
|
|
|
|
$projectId = (int)($_POST['project_id'] ?? 0);
|
|
$name = trim((string)($_POST['name'] ?? ''));
|
|
$description = visuCleanString($_POST['description'] ?? '');
|
|
$width = (int)($_POST['width'] ?? 1920);
|
|
$height = (int)($_POST['height'] ?? 1080);
|
|
|
|
if ($projectId <= 0 || $name === '') {
|
|
visuJson(['ok' => false, 'error' => 'Projekt oder Name fehlt.'], 400);
|
|
}
|
|
|
|
if ($width < 320 || $width > 10000) {
|
|
$width = 1920;
|
|
}
|
|
|
|
if ($height < 240 || $height > 10000) {
|
|
$height = 1080;
|
|
}
|
|
|
|
if (!visuProjectVisible($pdo, $projectId, $userId, $canViewAll)) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt nicht sichtbar.'], 403);
|
|
}
|
|
|
|
$slugBase = visuSlug($name);
|
|
$slug = $slugBase;
|
|
$i = 2;
|
|
|
|
while (true) {
|
|
$chk = $pdo->prepare("SELECT 1 FROM visu_pages WHERE project_id = ? AND slug = ? LIMIT 1");
|
|
$chk->execute([$projectId, $slug]);
|
|
|
|
if (!$chk->fetchColumn()) {
|
|
break;
|
|
}
|
|
|
|
$slug = $slugBase . '-' . $i;
|
|
$i++;
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO visu_pages
|
|
(project_id, name, slug, description, width, height, is_active, created_by)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, 1, ?)
|
|
");
|
|
$stmt->execute([$projectId, $name, $slug, $description, $width, $height, $userId]);
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'page_id' => (int)$pdo->lastInsertId(),
|
|
]);
|
|
}
|
|
|
|
if ($action === 'page_settings_save') {
|
|
if (!$canEditDesigner) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Bearbeiten.'], 403);
|
|
}
|
|
|
|
$pageId = (int)($_POST['page_id'] ?? 0);
|
|
$page = visuPageVisible($pdo, $pageId, $userId, $canViewAll);
|
|
|
|
if (!$page) {
|
|
visuJson(['ok' => false, 'error' => 'Seite nicht gefunden.'], 404);
|
|
}
|
|
|
|
$backgroundColor = trim((string)($_POST['background_color'] ?? '#0f172a'));
|
|
if (!preg_match('/^#[0-9a-fA-F]{6}$/', $backgroundColor)) {
|
|
$backgroundColor = '#0f172a';
|
|
}
|
|
|
|
$gridEnabled = !empty($_POST['grid_enabled']) ? 1 : 0;
|
|
$snapEnabled = !empty($_POST['snap_enabled']) ? 1 : 0;
|
|
$gridSize = (int)($_POST['grid_size'] ?? 40);
|
|
$width = (int)($_POST['width'] ?? $page['width'] ?? 1920);
|
|
$height = (int)($_POST['height'] ?? $page['height'] ?? 1080);
|
|
|
|
if ($gridSize < 5) {
|
|
$gridSize = 5;
|
|
}
|
|
|
|
if ($gridSize > 200) {
|
|
$gridSize = 200;
|
|
}
|
|
|
|
if ($width < 320 || $width > 10000) {
|
|
$width = (int)($page['width'] ?? 1920);
|
|
}
|
|
|
|
if ($height < 240 || $height > 10000) {
|
|
$height = (int)($page['height'] ?? 1080);
|
|
}
|
|
|
|
$backgroundImage = (string)($page['background_image'] ?? '');
|
|
$uploaded = visuSaveUploadedImage('background_image', VISU_BG_UPLOAD_DIR, VISU_BG_UPLOAD_URL);
|
|
|
|
if ($uploaded) {
|
|
visuDeleteUploadedFile($backgroundImage, VISU_BG_UPLOAD_URL, VISU_BG_UPLOAD_DIR);
|
|
$backgroundImage = $uploaded;
|
|
}
|
|
|
|
if (!empty($_POST['remove_background_image'])) {
|
|
visuDeleteUploadedFile($backgroundImage, VISU_BG_UPLOAD_URL, VISU_BG_UPLOAD_DIR);
|
|
$backgroundImage = null;
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
UPDATE visu_pages
|
|
SET
|
|
width = ?,
|
|
height = ?,
|
|
background_color = ?,
|
|
background_image = ?,
|
|
grid_enabled = ?,
|
|
grid_size = ?,
|
|
snap_enabled = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
");
|
|
$stmt->execute([
|
|
$width,
|
|
$height,
|
|
$backgroundColor,
|
|
$backgroundImage,
|
|
$gridEnabled,
|
|
$gridSize,
|
|
$snapEnabled,
|
|
$pageId,
|
|
]);
|
|
|
|
visuJson(['ok' => true]);
|
|
}
|
|
|
|
if ($action === 'page_delete') {
|
|
if (!$canEditDesigner) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Löschen.'], 403);
|
|
}
|
|
|
|
$pageId = (int)($_POST['page_id'] ?? 0);
|
|
$page = visuPageVisible($pdo, $pageId, $userId, $canViewAll);
|
|
|
|
if (!$page) {
|
|
visuJson(['ok' => false, 'error' => 'Seite nicht gefunden.'], 404);
|
|
}
|
|
|
|
$stmt = $pdo->prepare("UPDATE visu_pages SET is_active = 0 WHERE id = ?");
|
|
$stmt->execute([$pageId]);
|
|
|
|
visuJson(['ok' => true]);
|
|
}
|
|
|
|
if ($action === 'assets') {
|
|
$projectId = (int)($_GET['project_id'] ?? 0);
|
|
|
|
$params = [];
|
|
$where = ["1=1"];
|
|
|
|
if ($projectId > 0) {
|
|
if (!visuProjectVisible($pdo, $projectId, $userId, $canViewAll)) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt nicht sichtbar.'], 403);
|
|
}
|
|
|
|
$where[] = "(va.is_global = 1 OR va.project_id = :project_id)";
|
|
$params[':project_id'] = $projectId;
|
|
} else {
|
|
$where[] = "va.is_global = 1";
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
va.id,
|
|
va.project_id,
|
|
va.name,
|
|
va.description,
|
|
va.asset_type,
|
|
va.is_global,
|
|
va.created_at
|
|
FROM visu_assets va
|
|
WHERE " . implode(' AND ', $where) . "
|
|
ORDER BY va.asset_type ASC, va.name ASC
|
|
");
|
|
$stmt->execute($params);
|
|
$assets = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$assetIds = array_map(fn($a) => (int)$a['id'], $assets);
|
|
$statesByAsset = visuLoadAssetStatesForAssets($pdo, $assetIds);
|
|
|
|
foreach ($assets as &$asset) {
|
|
$asset['states'] = $statesByAsset[(int)$asset['id']] ?? [];
|
|
}
|
|
unset($asset);
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'assets' => $assets,
|
|
'can_manage_assets' => $canManageAssets,
|
|
'can_edit_assets' => $canEditAssets,
|
|
'can_delete_assets' => $canDeleteAssets,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'image_library') {
|
|
if (!$canViewDesigner && !$canEditAssets) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Zugriff auf Bilder.'], 403);
|
|
}
|
|
|
|
$projectId = (int)($_GET['project_id'] ?? 0);
|
|
|
|
if ($projectId > 0 && !visuProjectVisible($pdo, $projectId, $userId, $canViewAll)) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt nicht sichtbar.'], 403);
|
|
}
|
|
|
|
try {
|
|
$stmt = $pdo->prepare("
|
|
INSERT IGNORE INTO visu_image_library
|
|
(project_id, file_url, original_name, created_by, is_active)
|
|
SELECT
|
|
MIN(va.project_id) AS project_id,
|
|
vas.image_file,
|
|
SUBSTRING_INDEX(vas.image_file, '/', -1) AS original_name,
|
|
? AS created_by,
|
|
1 AS is_active
|
|
FROM visu_asset_states vas
|
|
INNER JOIN visu_assets va ON va.id = vas.asset_id
|
|
WHERE vas.image_file LIKE ?
|
|
GROUP BY vas.image_file
|
|
");
|
|
$stmt->execute([$userId, rtrim(VISU_ASSET_UPLOAD_URL, '/') . '/%']);
|
|
|
|
$params = [];
|
|
$where = ['vil.is_active = 1'];
|
|
|
|
if ($projectId > 0) {
|
|
$where[] = '(vil.project_id IS NULL OR vil.project_id = :project_id)';
|
|
$params[':project_id'] = $projectId;
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
vil.id,
|
|
vil.project_id,
|
|
vil.file_url,
|
|
vil.original_name,
|
|
vil.mime_type,
|
|
vil.file_size,
|
|
vil.created_at,
|
|
(
|
|
SELECT COUNT(*)
|
|
FROM visu_asset_states vas
|
|
WHERE vas.image_file = vil.file_url
|
|
) AS usage_count
|
|
FROM visu_image_library vil
|
|
WHERE " . implode(' AND ', $where) . "
|
|
ORDER BY vil.created_at DESC, vil.id DESC
|
|
");
|
|
$stmt->execute($params);
|
|
$images = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (Throwable $e) {
|
|
visuJson(['ok' => false, 'error' => 'Tabelle visu_image_library fehlt oder ist nicht erreichbar.'], 500);
|
|
}
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'images' => $images,
|
|
'can_edit' => $canEditAssets,
|
|
'can_delete' => $canDeleteAssets,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'image_library_upload') {
|
|
if (!$canEditAssets) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Hochladen von Bildern.'], 403);
|
|
}
|
|
|
|
$projectId = (int)($_POST['project_id'] ?? 0);
|
|
$libraryProjectId = $projectId > 0 ? $projectId : null;
|
|
|
|
if ($projectId > 0 && !visuProjectVisible($pdo, $projectId, $userId, $canViewAll)) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt nicht sichtbar.'], 403);
|
|
}
|
|
|
|
$files = $_FILES['images'] ?? null;
|
|
$uploaded = [];
|
|
|
|
if (!$files || !is_array($files)) {
|
|
visuJson(['ok' => false, 'error' => 'Keine Bilder uebergeben.'], 400);
|
|
}
|
|
|
|
if (is_array($files['name'] ?? null)) {
|
|
$count = count($files['name']);
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$file = [
|
|
'name' => $files['name'][$i] ?? '',
|
|
'type' => $files['type'][$i] ?? '',
|
|
'tmp_name' => $files['tmp_name'][$i] ?? '',
|
|
'error' => $files['error'][$i] ?? UPLOAD_ERR_NO_FILE,
|
|
'size' => $files['size'][$i] ?? 0,
|
|
];
|
|
$saved = visuSaveUploadedImageFile($file, VISU_ASSET_UPLOAD_DIR, VISU_ASSET_UPLOAD_URL);
|
|
if ($saved) {
|
|
visuImageLibraryRegister($pdo, $saved['url'], $saved['original_name'], $saved['mime_type'], $saved['file_size'], $libraryProjectId, $userId);
|
|
$uploaded[] = $saved;
|
|
}
|
|
}
|
|
} else {
|
|
$saved = visuSaveUploadedImageFile($files, VISU_ASSET_UPLOAD_DIR, VISU_ASSET_UPLOAD_URL);
|
|
if ($saved) {
|
|
visuImageLibraryRegister($pdo, $saved['url'], $saved['original_name'], $saved['mime_type'], $saved['file_size'], $libraryProjectId, $userId);
|
|
$uploaded[] = $saved;
|
|
}
|
|
}
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'images' => $uploaded,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'image_library_delete') {
|
|
if (!$canDeleteAssets) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Loeschen von Bildern.'], 403);
|
|
}
|
|
|
|
$imageId = (int)($_POST['image_id'] ?? 0);
|
|
|
|
if ($imageId <= 0) {
|
|
visuJson(['ok' => false, 'error' => 'Bild fehlt.'], 400);
|
|
}
|
|
|
|
try {
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
vil.file_url,
|
|
(
|
|
SELECT COUNT(*)
|
|
FROM visu_asset_states vas
|
|
WHERE vas.image_file = vil.file_url
|
|
) AS usage_count
|
|
FROM visu_image_library vil
|
|
WHERE vil.id = ?
|
|
AND vil.is_active = 1
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([$imageId]);
|
|
$image = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
} catch (Throwable $e) {
|
|
visuJson(['ok' => false, 'error' => 'Bilderbibliothek ist nicht erreichbar.'], 500);
|
|
}
|
|
|
|
if (!$image) {
|
|
visuJson(['ok' => false, 'error' => 'Bild nicht gefunden.'], 404);
|
|
}
|
|
|
|
if ((int)($image['usage_count'] ?? 0) > 0) {
|
|
visuJson(['ok' => false, 'error' => 'Bild wird noch verwendet und kann nicht geloescht werden.'], 409);
|
|
}
|
|
|
|
$stmt = $pdo->prepare("UPDATE visu_image_library SET is_active = 0 WHERE id = ?");
|
|
$stmt->execute([$imageId]);
|
|
visuDeleteUploadedFile((string)$image['file_url'], VISU_ASSET_UPLOAD_URL, VISU_ASSET_UPLOAD_DIR);
|
|
|
|
visuJson(['ok' => true]);
|
|
}
|
|
|
|
if ($action === 'asset_create') {
|
|
if (!$canEditAssets) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Erstellen von Symbolen.'], 403);
|
|
}
|
|
|
|
$name = trim((string)($_POST['name'] ?? ''));
|
|
$description = visuCleanString($_POST['description'] ?? '');
|
|
$assetType = (string)($_POST['asset_type'] ?? 'custom');
|
|
$projectId = (int)($_POST['project_id'] ?? 0);
|
|
$isGlobal = !empty($_POST['is_global']) ? 1 : 0;
|
|
|
|
if ($name === '') {
|
|
visuJson(['ok' => false, 'error' => 'Name fehlt.'], 400);
|
|
}
|
|
|
|
if (!in_array($assetType, ['background', 'pump', 'heat_generation', 'cold_generation', 'heat_recovery', 'valve', 'fan', 'sensor', 'text', 'custom'], true)) {
|
|
$assetType = 'custom';
|
|
}
|
|
|
|
if ($projectId > 0 && !visuProjectVisible($pdo, $projectId, $userId, $canViewAll)) {
|
|
visuJson(['ok' => false, 'error' => 'Projekt nicht sichtbar.'], 403);
|
|
}
|
|
|
|
if ($projectId <= 0) {
|
|
$projectId = null;
|
|
$isGlobal = 1;
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO visu_assets
|
|
(project_id, name, description, asset_type, is_global, created_by)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?)
|
|
");
|
|
$stmt->execute([$projectId, $name, $description, $assetType, $isGlobal, $userId]);
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'asset_id' => (int)$pdo->lastInsertId(),
|
|
]);
|
|
}
|
|
|
|
if ($action === 'asset_update') {
|
|
if (!$canEditAssets) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Bearbeiten von Symbolen.'], 403);
|
|
}
|
|
|
|
$assetId = (int)($_POST['asset_id'] ?? 0);
|
|
$name = trim((string)($_POST['name'] ?? ''));
|
|
$description = visuCleanString($_POST['description'] ?? '');
|
|
$assetType = (string)($_POST['asset_type'] ?? 'custom');
|
|
$isGlobal = !empty($_POST['is_global']) ? 1 : 0;
|
|
|
|
if ($assetId <= 0 || $name === '') {
|
|
visuJson(['ok' => false, 'error' => 'Symbol oder Name fehlt.'], 400);
|
|
}
|
|
|
|
if (!in_array($assetType, ['background', 'pump', 'heat_generation', 'cold_generation', 'heat_recovery', 'valve', 'fan', 'sensor', 'text', 'custom'], true)) {
|
|
$assetType = 'custom';
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
UPDATE visu_assets
|
|
SET
|
|
name = ?,
|
|
description = ?,
|
|
asset_type = ?,
|
|
is_global = ?
|
|
WHERE id = ?
|
|
");
|
|
$stmt->execute([$name, $description, $assetType, $isGlobal, $assetId]);
|
|
|
|
visuJson(['ok' => true, 'asset_id' => $assetId]);
|
|
}
|
|
|
|
if ($action === 'asset_delete') {
|
|
if (!$canDeleteAssets) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Löschen von Symbolen.'], 403);
|
|
}
|
|
|
|
$assetId = (int)($_POST['asset_id'] ?? 0);
|
|
|
|
if ($assetId <= 0) {
|
|
visuJson(['ok' => false, 'error' => 'Symbol fehlt.'], 400);
|
|
}
|
|
|
|
$stmt = $pdo->prepare("SELECT image_file FROM visu_asset_states WHERE asset_id = ?");
|
|
$stmt->execute([$assetId]);
|
|
$files = array_map('strval', $stmt->fetchAll(PDO::FETCH_COLUMN));
|
|
|
|
$stmt = $pdo->prepare("DELETE FROM visu_assets WHERE id = ?");
|
|
$stmt->execute([$assetId]);
|
|
|
|
foreach ($files as $file) {
|
|
visuDeleteAssetImageIfUnused($pdo, $file);
|
|
}
|
|
|
|
visuJson(['ok' => true]);
|
|
}
|
|
|
|
if ($action === 'asset_state_create' || $action === 'asset_state_update') {
|
|
if (!$canEditAssets) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Bearbeiten von Zuständen.'], 403);
|
|
}
|
|
|
|
$stateId = (int)($_POST['state_id'] ?? 0);
|
|
$assetId = (int)($_POST['asset_id'] ?? 0);
|
|
$label = trim((string)($_POST['label'] ?? ''));
|
|
$stateKey = trim((string)($_POST['state_key'] ?? ''));
|
|
$sortOrder = (int)($_POST['sort_order'] ?? 100);
|
|
$isDefault = !empty($_POST['is_default']) ? 1 : 0;
|
|
$libraryUrl = visuCleanString($_POST['image_library_url'] ?? '');
|
|
|
|
if ($assetId <= 0 || $label === '') {
|
|
visuJson(['ok' => false, 'error' => 'Symbol oder Zustandsname fehlt.'], 400);
|
|
}
|
|
|
|
$stmt = $pdo->prepare("SELECT project_id FROM visu_assets WHERE id = ? LIMIT 1");
|
|
$stmt->execute([$assetId]);
|
|
$assetProjectId = (int)($stmt->fetchColumn() ?: 0);
|
|
|
|
$stateKey = $stateKey === '' ? visuSlug($label) : visuSlug($stateKey);
|
|
$uploaded = visuSaveUploadedImage('image_file', VISU_ASSET_UPLOAD_DIR, VISU_ASSET_UPLOAD_URL);
|
|
if ($uploaded) {
|
|
$uploadFile = $_FILES['image_file'] ?? [];
|
|
visuImageLibraryRegister(
|
|
$pdo,
|
|
$uploaded,
|
|
(string)($uploadFile['name'] ?? ''),
|
|
(string)($uploadFile['type'] ?? ''),
|
|
(int)($uploadFile['size'] ?? 0),
|
|
$assetProjectId > 0 ? $assetProjectId : null,
|
|
$userId
|
|
);
|
|
}
|
|
|
|
if (!$uploaded && $libraryUrl && !visuImageLibraryAllowed($pdo, $libraryUrl, $assetProjectId, $userId, $canViewAll)) {
|
|
visuJson(['ok' => false, 'error' => 'Bild aus der Bibliothek ist nicht erlaubt.'], 403);
|
|
}
|
|
|
|
$nextImageFile = $uploaded ?: ($libraryUrl ?: '');
|
|
|
|
if ($action === 'asset_state_create') {
|
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM visu_asset_states WHERE asset_id = ?");
|
|
$stmt->execute([$assetId]);
|
|
if ((int)$stmt->fetchColumn() === 0) {
|
|
$isDefault = 1;
|
|
}
|
|
|
|
if ($isDefault) {
|
|
$stmt = $pdo->prepare("UPDATE visu_asset_states SET is_default = 0 WHERE asset_id = ?");
|
|
$stmt->execute([$assetId]);
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO visu_asset_states
|
|
(asset_id, state_key, label, image_file, sort_order, is_default)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?)
|
|
");
|
|
$stmt->execute([$assetId, $stateKey, $label, $nextImageFile, $sortOrder, $isDefault]);
|
|
|
|
visuJson(['ok' => true, 'state_id' => (int)$pdo->lastInsertId()]);
|
|
}
|
|
|
|
if ($stateId <= 0) {
|
|
visuJson(['ok' => false, 'error' => 'Zustand fehlt.'], 400);
|
|
}
|
|
|
|
$stmt = $pdo->prepare("SELECT image_file FROM visu_asset_states WHERE id = ? AND asset_id = ? LIMIT 1");
|
|
$stmt->execute([$stateId, $assetId]);
|
|
$oldState = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$oldState) {
|
|
visuJson(['ok' => false, 'error' => 'Zustand nicht gefunden.'], 404);
|
|
}
|
|
|
|
$oldFile = (string)($oldState['image_file'] ?? '');
|
|
|
|
if ($isDefault) {
|
|
$stmt = $pdo->prepare("UPDATE visu_asset_states SET is_default = 0 WHERE asset_id = ?");
|
|
$stmt->execute([$assetId]);
|
|
}
|
|
|
|
if ($nextImageFile !== '') {
|
|
$stmt = $pdo->prepare("
|
|
UPDATE visu_asset_states
|
|
SET
|
|
state_key = ?,
|
|
label = ?,
|
|
image_file = ?,
|
|
sort_order = ?,
|
|
is_default = ?
|
|
WHERE id = ?
|
|
AND asset_id = ?
|
|
");
|
|
$stmt->execute([$stateKey, $label, $nextImageFile, $sortOrder, $isDefault, $stateId, $assetId]);
|
|
if ($oldFile !== $nextImageFile) {
|
|
visuDeleteAssetImageIfUnused($pdo, (string)$oldFile, $stateId);
|
|
}
|
|
} else {
|
|
$stmt = $pdo->prepare("
|
|
UPDATE visu_asset_states
|
|
SET
|
|
state_key = ?,
|
|
label = ?,
|
|
sort_order = ?,
|
|
is_default = ?
|
|
WHERE id = ?
|
|
AND asset_id = ?
|
|
");
|
|
$stmt->execute([$stateKey, $label, $sortOrder, $isDefault, $stateId, $assetId]);
|
|
}
|
|
|
|
visuJson(['ok' => true, 'state_id' => $stateId]);
|
|
}
|
|
|
|
if ($action === 'asset_state_delete') {
|
|
if (!$canDeleteAssets) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Löschen von Zuständen.'], 403);
|
|
}
|
|
|
|
$stateId = (int)($_POST['state_id'] ?? 0);
|
|
|
|
if ($stateId <= 0) {
|
|
visuJson(['ok' => false, 'error' => 'Zustand fehlt.'], 400);
|
|
}
|
|
|
|
$stmt = $pdo->prepare("SELECT image_file FROM visu_asset_states WHERE id = ? LIMIT 1");
|
|
$stmt->execute([$stateId]);
|
|
$file = $stmt->fetchColumn();
|
|
|
|
$stmt = $pdo->prepare("DELETE FROM visu_asset_states WHERE id = ?");
|
|
$stmt->execute([$stateId]);
|
|
|
|
visuDeleteAssetImageIfUnused($pdo, (string)$file);
|
|
|
|
visuJson(['ok' => true]);
|
|
}
|
|
|
|
if ($action === 'elements_save') {
|
|
if (!$canEditDesigner) {
|
|
visuJson(['ok' => false, 'error' => 'Kein Recht zum Speichern.'], 403);
|
|
}
|
|
|
|
$pageId = (int)($_POST['page_id'] ?? 0);
|
|
$elementsRaw = (string)($_POST['elements_json'] ?? '[]');
|
|
|
|
$page = visuPageVisible($pdo, $pageId, $userId, $canViewAll);
|
|
|
|
if (!$page) {
|
|
visuJson(['ok' => false, 'error' => 'Seite nicht sichtbar.'], 403);
|
|
}
|
|
|
|
$elements = json_decode($elementsRaw, true);
|
|
|
|
if (!is_array($elements)) {
|
|
visuJson(['ok' => false, 'error' => 'Ungültiges Elements-JSON.'], 400);
|
|
}
|
|
|
|
$keepIds = [];
|
|
|
|
try {
|
|
$pdo->beginTransaction();
|
|
|
|
foreach ($elements as $el) {
|
|
if (!is_array($el)) {
|
|
continue;
|
|
}
|
|
|
|
$id = (int)($el['id'] ?? 0);
|
|
$elementType = (string)($el['element_type'] ?? 'asset');
|
|
|
|
if (!in_array($elementType, ['asset', 'value', 'text', 'shape', 'image'], true)) {
|
|
$elementType = 'asset';
|
|
}
|
|
|
|
$name = visuCleanString($el['name'] ?? null);
|
|
$label = visuCleanString($el['label'] ?? null);
|
|
$variableId = isset($el['variable_id']) && (int)$el['variable_id'] > 0 ? (int)$el['variable_id'] : null;
|
|
$assetId = isset($el['asset_id']) && (int)$el['asset_id'] > 0 ? (int)$el['asset_id'] : null;
|
|
|
|
$x = (float)($el['x'] ?? 0);
|
|
$y = (float)($el['y'] ?? 0);
|
|
$w = max(5, (float)($el['w'] ?? 120));
|
|
$h = max(5, (float)($el['h'] ?? 120));
|
|
$rotation = (float)($el['rotation'] ?? 0);
|
|
$zIndex = (int)($el['z_index'] ?? 10);
|
|
$opacity = min(1, max(0, (float)($el['opacity'] ?? 1)));
|
|
$isLocked = !empty($el['is_locked']) ? 1 : 0;
|
|
|
|
$stateRules = $el['state_rules'] ?? null;
|
|
$styleJson = $el['style_json'] ?? null;
|
|
|
|
$stateRulesJson = $stateRules === null ? null : json_encode($stateRules, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
$styleJsonText = $styleJson === null ? null : json_encode($styleJson, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
if ($id > 0) {
|
|
$stmt = $pdo->prepare("
|
|
UPDATE visu_elements
|
|
SET
|
|
asset_id = ?,
|
|
element_type = ?,
|
|
name = ?,
|
|
variable_id = ?,
|
|
label = ?,
|
|
x = ?,
|
|
y = ?,
|
|
w = ?,
|
|
h = ?,
|
|
rotation = ?,
|
|
z_index = ?,
|
|
opacity = ?,
|
|
state_rules = ?,
|
|
style_json = ?,
|
|
is_locked = ?
|
|
WHERE id = ?
|
|
AND page_id = ?
|
|
");
|
|
$stmt->execute([
|
|
$assetId,
|
|
$elementType,
|
|
$name,
|
|
$variableId,
|
|
$label,
|
|
$x,
|
|
$y,
|
|
$w,
|
|
$h,
|
|
$rotation,
|
|
$zIndex,
|
|
$opacity,
|
|
$stateRulesJson,
|
|
$styleJsonText,
|
|
$isLocked,
|
|
$id,
|
|
$pageId,
|
|
]);
|
|
|
|
$dbElementId = $id;
|
|
} else {
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO visu_elements
|
|
(
|
|
page_id,
|
|
asset_id,
|
|
element_type,
|
|
name,
|
|
variable_id,
|
|
label,
|
|
x,
|
|
y,
|
|
w,
|
|
h,
|
|
rotation,
|
|
z_index,
|
|
opacity,
|
|
state_rules,
|
|
style_json,
|
|
is_locked
|
|
)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
");
|
|
$stmt->execute([
|
|
$pageId,
|
|
$assetId,
|
|
$elementType,
|
|
$name,
|
|
$variableId,
|
|
$label,
|
|
$x,
|
|
$y,
|
|
$w,
|
|
$h,
|
|
$rotation,
|
|
$zIndex,
|
|
$opacity,
|
|
$stateRulesJson,
|
|
$styleJsonText,
|
|
$isLocked,
|
|
]);
|
|
|
|
$dbElementId = (int)$pdo->lastInsertId();
|
|
}
|
|
|
|
$keepIds[] = $dbElementId;
|
|
|
|
$stmt = $pdo->prepare("DELETE FROM visu_element_state_rules WHERE element_id = ?");
|
|
$stmt->execute([$dbElementId]);
|
|
|
|
$rules = $el['rules'] ?? [];
|
|
|
|
if (is_array($rules)) {
|
|
foreach ($rules as $rule) {
|
|
if (!is_array($rule)) {
|
|
continue;
|
|
}
|
|
|
|
$assetStateId = (int)($rule['asset_state_id'] ?? 0);
|
|
|
|
if ($assetStateId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$variableType = (string)($rule['variable_type'] ?? 'digital');
|
|
|
|
if (!in_array($variableType, ['analog', 'digital', 'alarm', 'setpoint'], true)) {
|
|
$variableType = 'digital';
|
|
}
|
|
|
|
$operator = (string)($rule['operator'] ?? 'eq');
|
|
|
|
if (!in_array($operator, ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'contains', 'not_contains', 'truthy', 'falsy'], true)) {
|
|
$operator = 'eq';
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO visu_element_state_rules
|
|
(
|
|
element_id,
|
|
asset_state_id,
|
|
variable_type,
|
|
external_id,
|
|
variable_id,
|
|
point_key,
|
|
point_label,
|
|
operator,
|
|
compare_value,
|
|
priority
|
|
)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
");
|
|
$stmt->execute([
|
|
$dbElementId,
|
|
$assetStateId,
|
|
$variableType,
|
|
visuCleanString($rule['external_id'] ?? null),
|
|
!empty($rule['variable_id']) ? (int)$rule['variable_id'] : null,
|
|
visuCleanString($rule['point_key'] ?? null),
|
|
visuCleanString($rule['point_label'] ?? null),
|
|
$operator,
|
|
visuCleanString($rule['compare_value'] ?? null),
|
|
(int)($rule['priority'] ?? 100),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($keepIds)) {
|
|
$ph = implode(',', array_fill(0, count($keepIds), '?'));
|
|
$params = array_merge([$pageId], $keepIds);
|
|
|
|
$stmt = $pdo->prepare("DELETE FROM visu_elements WHERE page_id = ? AND id NOT IN ($ph)");
|
|
$stmt->execute($params);
|
|
} else {
|
|
$stmt = $pdo->prepare("DELETE FROM visu_elements WHERE page_id = ?");
|
|
$stmt->execute([$pageId]);
|
|
}
|
|
|
|
$stmt = $pdo->prepare("UPDATE visu_pages SET updated_at = CURRENT_TIMESTAMP WHERE id = ?");
|
|
$stmt->execute([$pageId]);
|
|
|
|
$pdo->commit();
|
|
} catch (Throwable $e) {
|
|
if ($pdo->inTransaction()) {
|
|
$pdo->rollBack();
|
|
}
|
|
|
|
visuJson(['ok' => false, 'error' => $e->getMessage()], 500);
|
|
}
|
|
|
|
visuJson(['ok' => true]);
|
|
}
|
|
|
|
if ($action === 'live_values') {
|
|
$pageId = (int)($_GET['page_id'] ?? 0);
|
|
$page = visuPageVisible($pdo, $pageId, $userId, $canViewAll);
|
|
|
|
if (!$page) {
|
|
visuJson(['ok' => false, 'error' => 'Seite nicht sichtbar.'], 403);
|
|
}
|
|
|
|
$values = [];
|
|
foreach (visuGetProjectPointsWithVirtual($pdo, (int)$page['project_id'], true) as $point) {
|
|
$values[$point['source_key']] = $point;
|
|
|
|
if (!empty($point['variable_id'])) {
|
|
$variableKey = (string)$point['variable_id'];
|
|
|
|
if (
|
|
!isset($values[$variableKey])
|
|
|| !visuPointHasDisplayValue($values[$variableKey])
|
|
|| visuPointHasDisplayValue($point)
|
|
) {
|
|
$values[$variableKey] = $point;
|
|
}
|
|
}
|
|
}
|
|
|
|
visuJson([
|
|
'ok' => true,
|
|
'values' => $values,
|
|
'can_write_setpoints' => $canWriteSetpoints,
|
|
]);
|
|
}
|
|
|
|
function canAccessVisuPage(PDO $pdo, int $pageId, int $userId, bool $canViewAll): bool
|
|
{
|
|
if ($pageId <= 0 || $userId <= 0) {
|
|
return false;
|
|
}
|
|
|
|
if ($canViewAll) {
|
|
$stmt = $pdo->prepare("
|
|
SELECT 1
|
|
FROM visu_pages vp
|
|
WHERE vp.id = ?
|
|
AND vp.is_active = 1
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([$pageId]);
|
|
return (bool)$stmt->fetchColumn();
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT 1
|
|
FROM visu_pages vp
|
|
INNER JOIN projects p ON p.id = vp.project_id
|
|
WHERE vp.id = ?
|
|
AND vp.is_active = 1
|
|
AND (
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM project_assignments pa
|
|
WHERE pa.project_id = p.id
|
|
AND pa.user_id = ?
|
|
)
|
|
OR EXISTS (
|
|
SELECT 1
|
|
FROM project_groups pg
|
|
JOIN roles r ON r.key_name = pg.group_name
|
|
JOIN user_roles ur ON ur.role_id = r.id
|
|
WHERE pg.project_id = p.id
|
|
AND ur.user_id = ?
|
|
)
|
|
)
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([$pageId, $userId, $userId]);
|
|
|
|
return (bool)$stmt->fetchColumn();
|
|
}
|
|
|
|
|
|
|
|
if ($action === 'set_value') {
|
|
if (!$canWriteSetpoints) {
|
|
http_response_code(403);
|
|
echo json_encode([
|
|
'ok' => false,
|
|
'error' => 'Keine Berechtigung zum Schreiben von Sollwerten.'
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
$pageId = (int)($_POST['page_id'] ?? 0);
|
|
$externalId = trim((string)($_POST['external_id'] ?? ''));
|
|
$variableId = (int)($_POST['variable_id'] ?? 0);
|
|
$pointKey = trim((string)($_POST['point_key'] ?? ''));
|
|
$value = trim((string)($_POST['value'] ?? ''));
|
|
|
|
if ($pageId <= 0 || $value === '') {
|
|
http_response_code(400);
|
|
echo json_encode([
|
|
'ok' => false,
|
|
'error' => 'Ungültige Sollwertdaten.'
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
if (!canAccessVisuPage($pdo, $pageId, $userId, $canViewAll)) {
|
|
http_response_code(403);
|
|
echo json_encode([
|
|
'ok' => false,
|
|
'error' => 'Kein Zugriff auf diese Visualisierung.'
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
/*
|
|
Wichtig:
|
|
Für Symcon/API schreiben wir bevorzugt über variable_id.
|
|
Falls deine getsetpoints stattdessen point_key/external_id braucht,
|
|
bleibt beides im Payload drin.
|
|
*/
|
|
$postData = [
|
|
'api_key' => $mainApiKey,
|
|
'function' => 'setvalue',
|
|
'value' => $value,
|
|
];
|
|
|
|
if ($variableId > 0) {
|
|
$postData['id'] = (string)$variableId;
|
|
}
|
|
|
|
if ($externalId !== '') {
|
|
$postData['external_id'] = $externalId;
|
|
}
|
|
|
|
if ($pointKey !== '') {
|
|
$postData['point_key'] = $pointKey;
|
|
}
|
|
|
|
$ch = curl_init('https://main.api.se-inno.de');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => $postData,
|
|
CURLOPT_TIMEOUT => 15,
|
|
]);
|
|
|
|
$resp = curl_exec($ch);
|
|
$err = curl_error($ch);
|
|
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($err) {
|
|
http_response_code(502);
|
|
echo json_encode([
|
|
'ok' => false,
|
|
'error' => 'Upstream Fehler: ' . $err
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
$raw = trim((string)$resp);
|
|
|
|
/*
|
|
main.api kann JSON liefern, muss aber nicht.
|
|
Deshalb akzeptieren wir auch:
|
|
OK, 1, true, saved, success usw.
|
|
*/
|
|
$decoded = json_decode($raw, true);
|
|
|
|
if (is_array($decoded)) {
|
|
$upstreamOk =
|
|
($decoded['ok'] ?? null) === true ||
|
|
($decoded['success'] ?? null) === true ||
|
|
(string)($decoded['status'] ?? '') === 'ok' ||
|
|
(string)($decoded['result'] ?? '') === 'ok';
|
|
|
|
if (!$upstreamOk && isset($decoded['error'])) {
|
|
http_response_code(502);
|
|
echo json_encode([
|
|
'ok' => false,
|
|
'error' => 'Upstream: ' . (is_string($decoded['error']) ? $decoded['error'] : json_encode($decoded['error'])),
|
|
'raw' => $raw,
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
echo json_encode([
|
|
'ok' => true,
|
|
'upstream' => $decoded,
|
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
$okRawValues = ['ok', '1', 'true', 'success', 'saved', 'gespeichert'];
|
|
|
|
if (
|
|
$code >= 200 &&
|
|
$code < 300 &&
|
|
(
|
|
$raw === '' ||
|
|
in_array(mb_strtolower($raw), $okRawValues, true) ||
|
|
str_contains(mb_strtolower($raw), 'ok') ||
|
|
str_contains(mb_strtolower($raw), 'success')
|
|
)
|
|
) {
|
|
echo json_encode([
|
|
'ok' => true,
|
|
'raw' => $raw,
|
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
http_response_code(502);
|
|
echo json_encode([
|
|
'ok' => false,
|
|
'error' => 'Upstream hat keine gültige Erfolgsantwort geliefert.',
|
|
'http_code' => $code,
|
|
'raw' => $raw,
|
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
visuJson(['ok' => false, 'error' => 'Unbekannte Aktion.'], 400);
|