Compare commits
29 Commits
9ab8f8b4bf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d55075490 | |||
| 867da15823 | |||
| 0abd37d7a5 | |||
| 7abd2122fe | |||
| cb3da68673 | |||
| ea4738d9eb | |||
| 672645673f | |||
| 22665948e4 | |||
| 0b829e9dfe | |||
| faffe3d874 | |||
| bf0f596ad4 | |||
| 3ecc6275bc | |||
| d0397e475e | |||
| 51d9f7fa22 | |||
| 80dc3b40d3 | |||
| e15dd14cab | |||
| 54d66da52d | |||
| ae359048af | |||
| c678660ad6 | |||
| c952fc40bc | |||
| b2c391539d | |||
| e7b14406fb | |||
| 4707925917 | |||
| c2c9cfe709 | |||
| 9411337939 | |||
| 986cafcfd6 | |||
| c12577f4f8 | |||
| f7b1adc015 | |||
| 0f307c7eca |
@@ -53,7 +53,7 @@ class NotInternal implements Validator
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->hostCheck->isNotInternalHost($value)) {
|
if (!$this->hostCheck->isHostAndNotInternal($value)) {
|
||||||
return Failure::create();
|
return Failure::create();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class NotInternal implements Validator
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->urlCheck->isNotInternalUrl($value)) {
|
if (!$this->urlCheck->isUrlAndNotIternal($value)) {
|
||||||
return Failure::create();
|
return Failure::create();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,9 @@ class MassUpdate implements MassAction
|
|||||||
|
|
||||||
private function clearRoleCache(string $id): void
|
private function clearRoleCache(string $id): void
|
||||||
{
|
{
|
||||||
$this->fileManager->removeFile('data/cache/application/acl/' . $id . '.php');
|
$part = basename($id);
|
||||||
|
|
||||||
|
$this->fileManager->removeFile("data/cache/application/acl/$part.php");
|
||||||
}
|
}
|
||||||
|
|
||||||
private function clearPortalRolesCache(): void
|
private function clearPortalRolesCache(): void
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class BeforeSaveValidateHosts implements SaveHook
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->hostCheck->isNotInternalHost($host)) {
|
if (!$this->hostCheck->isHostAndNotInternal($host)) {
|
||||||
$message = $this->composeErrorMessage($host, $address);
|
$message = $this->composeErrorMessage($host, $address);
|
||||||
|
|
||||||
throw new Forbidden($message);
|
throw new Forbidden($message);
|
||||||
@@ -97,7 +97,11 @@ class BeforeSaveValidateHosts implements SaveHook
|
|||||||
|
|
||||||
$address = $host . ':' . $port;
|
$address = $host . ':' . $port;
|
||||||
|
|
||||||
if (!$this->hostCheck->isNotInternalHost($host)) {
|
if (in_array($address, $this->getAllowedAddressList())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->hostCheck->isHostAndNotInternal($host)) {
|
||||||
$message = $this->composeErrorMessage($host, $address);
|
$message = $this->composeErrorMessage($host, $address);
|
||||||
|
|
||||||
throw new Forbidden($message);
|
throw new Forbidden($message);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ namespace Espo\Classes\TemplateHelpers;
|
|||||||
use Espo\Core\Htmlizer\Helper;
|
use Espo\Core\Htmlizer\Helper;
|
||||||
use Espo\Core\Htmlizer\Helper\Data;
|
use Espo\Core\Htmlizer\Helper\Data;
|
||||||
use Espo\Core\Htmlizer\Helper\Result;
|
use Espo\Core\Htmlizer\Helper\Result;
|
||||||
use Michelf\MarkdownExtra as MarkdownTransformer;
|
use Espo\Core\Utils\Markdown\Markdown;
|
||||||
|
|
||||||
class MarkdownText implements Helper
|
class MarkdownText implements Helper
|
||||||
{
|
{
|
||||||
@@ -44,7 +44,7 @@ class MarkdownText implements Helper
|
|||||||
return Result::createEmpty();
|
return Result::createEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
$transformed = MarkdownTransformer::defaultTransform($value);
|
$transformed = Markdown::transform($value);
|
||||||
|
|
||||||
return Result::createSafeString($transformed);
|
return Result::createSafeString($transformed);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class Clearer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$part = $user->getId() . '.php';
|
$part = basename($user->getId() . '.php');
|
||||||
|
|
||||||
$this->fileManager->remove('data/cache/application/acl/' . $part);
|
$this->fileManager->remove('data/cache/application/acl/' . $part);
|
||||||
$this->fileManager->remove('data/cache/application/aclMap/' . $part);
|
$this->fileManager->remove('data/cache/application/aclMap/' . $part);
|
||||||
@@ -77,7 +77,7 @@ class Clearer
|
|||||||
->find();
|
->find();
|
||||||
|
|
||||||
foreach ($portals as $portal) {
|
foreach ($portals as $portal) {
|
||||||
$part = $portal->getId() . '/' . $user->getId() . '.php';
|
$part = basename($portal->getId()) . '/' . basename($user->getId() . '.php');
|
||||||
|
|
||||||
$this->fileManager->remove('data/cache/application/aclPortal/' . $part);
|
$this->fileManager->remove('data/cache/application/aclPortal/' . $part);
|
||||||
$this->fileManager->remove('data/cache/application/aclPortalMap/' . $part);
|
$this->fileManager->remove('data/cache/application/aclPortalMap/' . $part);
|
||||||
|
|||||||
@@ -115,7 +115,8 @@ class EspoUploadDir implements Storage, Local
|
|||||||
protected function getFilePath(Attachment $attachment)
|
protected function getFilePath(Attachment $attachment)
|
||||||
{
|
{
|
||||||
$sourceId = $attachment->getSourceId();
|
$sourceId = $attachment->getSourceId();
|
||||||
|
$file = basename($sourceId);
|
||||||
|
|
||||||
return 'data/upload/' . $sourceId;
|
return 'data/upload/' . $file;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ use Espo\Core\Formula\EvaluatedArgumentList;
|
|||||||
use Espo\Core\Formula\Exceptions\BadArgumentType;
|
use Espo\Core\Formula\Exceptions\BadArgumentType;
|
||||||
use Espo\Core\Formula\Exceptions\TooFewArguments;
|
use Espo\Core\Formula\Exceptions\TooFewArguments;
|
||||||
use Espo\Core\Formula\Func;
|
use Espo\Core\Formula\Func;
|
||||||
use Michelf\Markdown;
|
use Espo\Core\Utils\Markdown\Markdown;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @noinspection PhpUnused
|
* @noinspection PhpUnused
|
||||||
@@ -52,6 +52,6 @@ class TransformType implements Func
|
|||||||
throw BadArgumentType::create(1, 'string');
|
throw BadArgumentType::create(1, 'string');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Markdown::defaultTransform($string);
|
return Markdown::transform($string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class Service
|
|||||||
if (
|
if (
|
||||||
$params->getHost() &&
|
$params->getHost() &&
|
||||||
!$this->addressUtil->isAllowedAddress($params) &&
|
!$this->addressUtil->isAllowedAddress($params) &&
|
||||||
!$this->hostCheck->isNotInternalHost($params->getHost())
|
!$this->hostCheck->isHostAndNotInternal($params->getHost())
|
||||||
) {
|
) {
|
||||||
throw new Forbidden("Not allowed internal host.");
|
throw new Forbidden("Not allowed internal host.");
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ class Service
|
|||||||
if (
|
if (
|
||||||
$params->getHost() &&
|
$params->getHost() &&
|
||||||
!$this->addressUtil->isAllowedAddress($params) &&
|
!$this->addressUtil->isAllowedAddress($params) &&
|
||||||
!$this->hostCheck->isNotInternalHost($params->getHost())
|
!$this->hostCheck->isHostAndNotInternal($params->getHost())
|
||||||
) {
|
) {
|
||||||
throw new Forbidden("Not allowed internal host.");
|
throw new Forbidden("Not allowed internal host.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class Service
|
|||||||
if (
|
if (
|
||||||
$params->getHost() &&
|
$params->getHost() &&
|
||||||
!$this->addressUtil->isAllowedAddress($params) &&
|
!$this->addressUtil->isAllowedAddress($params) &&
|
||||||
!$this->hostCheck->isNotInternalHost($params->getHost())
|
!$this->hostCheck->isHostAndNotInternal($params->getHost())
|
||||||
) {
|
) {
|
||||||
throw new Forbidden("Not allowed internal host.");
|
throw new Forbidden("Not allowed internal host.");
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@ class Service
|
|||||||
if (
|
if (
|
||||||
$params->getHost() &&
|
$params->getHost() &&
|
||||||
!$this->addressUtil->isAllowedAddress($params) &&
|
!$this->addressUtil->isAllowedAddress($params) &&
|
||||||
!$this->hostCheck->isNotInternalHost($params->getHost())
|
!$this->hostCheck->isHostAndNotInternal($params->getHost())
|
||||||
) {
|
) {
|
||||||
throw new Forbidden("Not allowed host.");
|
throw new Forbidden("Not allowed host.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ class Starter extends StarterBase
|
|||||||
SystemConfig $systemConfig,
|
SystemConfig $systemConfig,
|
||||||
ApplicationState $applicationState
|
ApplicationState $applicationState
|
||||||
) {
|
) {
|
||||||
$routeCacheFile = 'data/cache/application/slim-routes-portal-' . $applicationState->getPortalId() . '.php';
|
$part = basename($applicationState->getPortalId());
|
||||||
|
|
||||||
|
$routeCacheFile = 'data/cache/application/slim-routes-portal-' . $part . '.php';
|
||||||
|
|
||||||
parent::__construct(
|
parent::__construct(
|
||||||
$requestProcessor,
|
$requestProcessor,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* This file is part of EspoCRM.
|
* This file is part of EspoCRM.
|
||||||
*
|
*
|
||||||
* EspoCRM – Open Source CRM application.
|
* EspoCRM – Open Source CRM application.
|
||||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
* Website: https://www.espocrm.com
|
* Website: https://www.espocrm.com
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
@@ -27,26 +27,24 @@
|
|||||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||||
************************************************************************/
|
************************************************************************/
|
||||||
|
|
||||||
namespace Espo\Core\Utils\Database\Dbal\Platforms\Keywords;
|
namespace Espo\Core\Utils\Markdown;
|
||||||
|
|
||||||
use Doctrine\DBAL\Platforms\Keywords\MariaDBKeywords;
|
use Michelf\Markdown as MarkdownParser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 'LEAD' happened to be a reserved words on some environments.
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class MariaDb102Keywords extends MariaDBKeywords
|
class Markdown
|
||||||
{
|
{
|
||||||
/** @deprecated */
|
/**
|
||||||
public function getName(): string
|
* @internal
|
||||||
|
*/
|
||||||
|
public static function transform(string $text): string
|
||||||
{
|
{
|
||||||
return 'MariaDb102';
|
$parser = new MarkdownParser();
|
||||||
}
|
$parser->no_markup = true;
|
||||||
|
$parser->no_entities = true;
|
||||||
|
|
||||||
protected function getKeywords(): array
|
return $parser->transform($text);
|
||||||
{
|
|
||||||
return [
|
|
||||||
...parent::getKeywords(),
|
|
||||||
'LEAD',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,30 +32,35 @@ namespace Espo\Core\Utils\Security;
|
|||||||
use const DNS_A;
|
use const DNS_A;
|
||||||
use const FILTER_FLAG_NO_PRIV_RANGE;
|
use const FILTER_FLAG_NO_PRIV_RANGE;
|
||||||
use const FILTER_FLAG_NO_RES_RANGE;
|
use const FILTER_FLAG_NO_RES_RANGE;
|
||||||
|
use const FILTER_FLAG_HOSTNAME;
|
||||||
|
use const FILTER_VALIDATE_DOMAIN;
|
||||||
use const FILTER_VALIDATE_IP;
|
use const FILTER_VALIDATE_IP;
|
||||||
|
|
||||||
class HostCheck
|
class HostCheck
|
||||||
{
|
{
|
||||||
public function isNotInternalHost(string $host): bool
|
/**
|
||||||
|
* Validates the string is a host and it's not internal.
|
||||||
|
* If not a host, returns false.
|
||||||
|
*
|
||||||
|
* @since 9.3.4
|
||||||
|
*/
|
||||||
|
public function isHostAndNotInternal(string $host): bool
|
||||||
{
|
{
|
||||||
$records = dns_get_record($host, DNS_A);
|
|
||||||
|
|
||||||
if (filter_var($host, FILTER_VALIDATE_IP)) {
|
if (filter_var($host, FILTER_VALIDATE_IP)) {
|
||||||
return $this->ipAddressIsNotInternal($host);
|
return $this->ipAddressIsNotInternal($host);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$records) {
|
if (!$this->isDomainHost($host)) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($records as $record) {
|
$ipAddresses = $this->getHostIpAddresses($host);
|
||||||
/** @var ?string $idAddress */
|
|
||||||
$idAddress = $record['ip'] ?? null;
|
|
||||||
|
|
||||||
if (!$idAddress) {
|
if ($ipAddresses === []) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($ipAddresses as $idAddress) {
|
||||||
if (!$this->ipAddressIsNotInternal($idAddress)) {
|
if (!$this->ipAddressIsNotInternal($idAddress)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -64,7 +69,66 @@ class HostCheck
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function ipAddressIsNotInternal(string $ipAddress): bool
|
/**
|
||||||
|
* @internal
|
||||||
|
* @since 9.3.4
|
||||||
|
*/
|
||||||
|
public function isDomainHost(string $host): bool
|
||||||
|
{
|
||||||
|
$normalized = $this->normalizeIpAddress($host);
|
||||||
|
|
||||||
|
if ($normalized !== false && filter_var($normalized, FILTER_VALIDATE_IP)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->hasNoNumericItem($host)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter_var($host, FILTER_VALIDATE_DOMAIN)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
* @internal
|
||||||
|
* @since 9.3.4
|
||||||
|
*/
|
||||||
|
public function getHostIpAddresses(string $host): array
|
||||||
|
{
|
||||||
|
$records = dns_get_record($host, DNS_A);
|
||||||
|
|
||||||
|
if (!$records) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$output = [];
|
||||||
|
|
||||||
|
foreach ($records as $record) {
|
||||||
|
/** @var ?string $idAddress */
|
||||||
|
$idAddress = $record['ip'] ?? null;
|
||||||
|
|
||||||
|
if (!$idAddress) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output[] = $idAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function ipAddressIsNotInternal(string $ipAddress): bool
|
||||||
{
|
{
|
||||||
return (bool) filter_var(
|
return (bool) filter_var(
|
||||||
$ipAddress,
|
$ipAddress,
|
||||||
@@ -72,4 +136,90 @@ class HostCheck
|
|||||||
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
|
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Since 9.3.4. Use `isHostAndNotInternal`.
|
||||||
|
* @todo Remove in 9.4.0.
|
||||||
|
*/
|
||||||
|
public function isNotInternalHost(string $host): bool
|
||||||
|
{
|
||||||
|
return $this->isHostAndNotInternal($host);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeIpAddress(string $ip): string|false
|
||||||
|
{
|
||||||
|
if (!str_contains($ip, '.')) {
|
||||||
|
return self::normalizePart($ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = explode('.', $ip);
|
||||||
|
|
||||||
|
if (count($parts) !== 4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if (preg_match('/^0x[0-9a-f]+$/i', $part)) {
|
||||||
|
$num = hexdec($part);
|
||||||
|
} else if (preg_match('/^0[0-7]+$/', $part) && $part !== '0') {
|
||||||
|
$num = octdec($part);
|
||||||
|
} else if (ctype_digit($part)) {
|
||||||
|
$num = (int)$part;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($num < 0 || $num > 255) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = $num;
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode('.', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizePart(string $ip): string|false
|
||||||
|
{
|
||||||
|
if (preg_match('/^0x[0-9a-f]+$/i', $ip)) {
|
||||||
|
$num = hexdec($ip);
|
||||||
|
} elseif (preg_match('/^0[0-7]+$/', $ip) && $ip !== '0') {
|
||||||
|
$num = octdec($ip);
|
||||||
|
} elseif (ctype_digit($ip)) {
|
||||||
|
$num = (int) $ip;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($num < 0 || $num > 0xFFFFFFFF) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$num = (int) $num;
|
||||||
|
|
||||||
|
return long2ip($num);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function hasNoNumericItem(string $host): bool
|
||||||
|
{
|
||||||
|
$hasNoNumeric = false;
|
||||||
|
|
||||||
|
foreach (explode('.', $host) as $it) {
|
||||||
|
if (!is_numeric($it) && !self::isHex($it)) {
|
||||||
|
$hasNoNumeric = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hasNoNumeric;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isHex(string $value): bool
|
||||||
|
{
|
||||||
|
return preg_match('/^0x[0-9a-fA-F]+$/', $value) === 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,6 @@
|
|||||||
|
|
||||||
namespace Espo\Core\Utils\Security;
|
namespace Espo\Core\Utils\Security;
|
||||||
|
|
||||||
use const FILTER_VALIDATE_URL;
|
|
||||||
use const PHP_URL_HOST;
|
|
||||||
|
|
||||||
class UrlCheck
|
class UrlCheck
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -44,9 +41,11 @@ class UrlCheck
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a URL does not follow to an internal host.
|
* Checks whether it's a URL, and it does not follow to an internal host.
|
||||||
|
*
|
||||||
|
* @since 9.3.4
|
||||||
*/
|
*/
|
||||||
public function isNotInternalUrl(string $url): bool
|
public function isUrlAndNotIternal(string $url): bool
|
||||||
{
|
{
|
||||||
if (!$this->isUrl($url)) {
|
if (!$this->isUrl($url)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -58,6 +57,118 @@ class UrlCheck
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->hostCheck->isNotInternalHost($host);
|
return $this->hostCheck->isHostAndNotInternal($host);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ?string[] Null if not a domain name or not a URL.
|
||||||
|
* @internal
|
||||||
|
* @since 9.3.4
|
||||||
|
*/
|
||||||
|
public function getCurlResolve(string $url): ?array
|
||||||
|
{
|
||||||
|
if (!$this->isUrl($url)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = parse_url($url, PHP_URL_HOST);
|
||||||
|
$port = parse_url($url, PHP_URL_PORT);
|
||||||
|
$scheme = parse_url($url, PHP_URL_SCHEME);
|
||||||
|
|
||||||
|
if ($port === null && $scheme) {
|
||||||
|
$port = match (strtolower($scheme)) {
|
||||||
|
'http' => 80,
|
||||||
|
'https'=> 443,
|
||||||
|
'ftp' => 21,
|
||||||
|
'ssh' => 22,
|
||||||
|
'smtp' => 25,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($port === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_string($host)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter_var($host, FILTER_VALIDATE_IP)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->hostCheck->isDomainHost($host)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ipAddresses = $this->hostCheck->getHostIpAddresses($host);
|
||||||
|
|
||||||
|
$output = [];
|
||||||
|
|
||||||
|
foreach ($ipAddresses as $ipAddress) {
|
||||||
|
$ipPart = $ipAddress;
|
||||||
|
|
||||||
|
if (filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
$ipPart = "[$ipPart]";
|
||||||
|
}
|
||||||
|
|
||||||
|
$output[] = "$host:$port:$ipPart";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Since 9.3.4. Use `isUrlAndNotIternal`.
|
||||||
|
* @todo Remove in 9.5.0.
|
||||||
|
*/
|
||||||
|
public function isNotInternalUrl(string $url): bool
|
||||||
|
{
|
||||||
|
return $this->isUrlAndNotIternal($url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $resolve
|
||||||
|
* @param string[] $allowed An allowed address list in the `{host}:{port}` format.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function validateCurlResolveNotInternal(array $resolve, array $allowed = []): bool
|
||||||
|
{
|
||||||
|
if ($resolve === []) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ipAddresses = [];
|
||||||
|
|
||||||
|
foreach ($resolve as $item) {
|
||||||
|
$arr = explode(':', $item, 3);
|
||||||
|
|
||||||
|
if (count($arr) < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ipAddress = $arr[2];
|
||||||
|
$port = $arr[1];
|
||||||
|
$domain = $arr[0];
|
||||||
|
|
||||||
|
if (in_array("$ipAddress:$port", $allowed) || in_array("$domain:$port", $allowed)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($ipAddress, '[') && str_ends_with($ipAddress, ']')) {
|
||||||
|
$ipAddress = substr($ipAddress, 1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ipAddresses[] = $ipAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($ipAddresses as $ipAddress) {
|
||||||
|
if (!$this->hostCheck->ipAddressIsNotInternal($ipAddress)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,13 @@ class TemplateFileManager
|
|||||||
?string $entityType = null
|
?string $entityType = null
|
||||||
): string {
|
): string {
|
||||||
|
|
||||||
|
$type = basename($type);
|
||||||
|
$language = basename($language);
|
||||||
|
$name = basename($name);
|
||||||
|
|
||||||
if ($entityType) {
|
if ($entityType) {
|
||||||
|
$entityType = basename($entityType);
|
||||||
|
|
||||||
return "custom/Espo/Custom/Resources/templates/{$type}/{$language}/{$entityType}/{$name}.tpl";
|
return "custom/Espo/Custom/Resources/templates/{$type}/{$language}/{$entityType}/{$name}.tpl";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +158,13 @@ class TemplateFileManager
|
|||||||
?string $entityType = null
|
?string $entityType = null
|
||||||
): string {
|
): string {
|
||||||
|
|
||||||
|
$type = basename($type);
|
||||||
|
$language = basename($language);
|
||||||
|
$name = basename($name);
|
||||||
|
|
||||||
if ($entityType) {
|
if ($entityType) {
|
||||||
|
$entityType = basename($entityType);
|
||||||
|
|
||||||
return "templates/{$type}/{$language}/{$entityType}/{$name}.tpl";
|
return "templates/{$type}/{$language}/{$entityType}/{$name}.tpl";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,11 +95,24 @@ class Sender
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!$this->addressUtil->isAllowedUrl($url) &&
|
!$this->addressUtil->isAllowedUrl($url) &&
|
||||||
!$this->urlCheck->isNotInternalUrl($url)
|
!$this->urlCheck->isUrlAndNotIternal($url)
|
||||||
) {
|
) {
|
||||||
throw new Error("URL '$url' points to an internal host, not allowed.");
|
throw new Error("URL '$url' points to an internal host, not allowed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$resolve = $this->urlCheck->getCurlResolve($url);
|
||||||
|
|
||||||
|
if ($resolve === []) {
|
||||||
|
throw new Error("Could not resolve the host.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var string[] $allowedAddressList */
|
||||||
|
$allowedAddressList = $this->config->get('webhookAllowedAddressList') ?? [];
|
||||||
|
|
||||||
|
if ($resolve !== null && !$this->urlCheck->validateCurlResolveNotInternal($resolve, $allowedAddressList)) {
|
||||||
|
throw new Error("Forbidden host.");
|
||||||
|
}
|
||||||
|
|
||||||
$handler = curl_init($url);
|
$handler = curl_init($url);
|
||||||
|
|
||||||
if ($handler === false) {
|
if ($handler === false) {
|
||||||
@@ -118,6 +131,10 @@ class Sender
|
|||||||
curl_setopt($handler, \CURLOPT_HTTPHEADER, $headerList);
|
curl_setopt($handler, \CURLOPT_HTTPHEADER, $headerList);
|
||||||
curl_setopt($handler, \CURLOPT_POSTFIELDS, $payload);
|
curl_setopt($handler, \CURLOPT_POSTFIELDS, $payload);
|
||||||
|
|
||||||
|
if ($resolve) {
|
||||||
|
curl_setopt($handler, CURLOPT_RESOLVE, $resolve);
|
||||||
|
}
|
||||||
|
|
||||||
curl_exec($handler);
|
curl_exec($handler);
|
||||||
|
|
||||||
$code = curl_getinfo($handler, \CURLINFO_HTTP_CODE);
|
$code = curl_getinfo($handler, \CURLINFO_HTTP_CODE);
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class Attachment implements EntryPoint
|
|||||||
$response
|
$response
|
||||||
->setHeader('Content-Length', (string) $size)
|
->setHeader('Content-Length', (string) $size)
|
||||||
->setHeader('Cache-Control', 'private, max-age=864000, immutable')
|
->setHeader('Cache-Control', 'private, max-age=864000, immutable')
|
||||||
->setHeader('Content-Security-Policy', "default-src 'self'")
|
->setHeader('Content-Security-Policy', "default-src 'self'; script-src 'none'; object-src 'none';")
|
||||||
->setBody($stream);
|
->setBody($stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class Download implements EntryPoint
|
|||||||
if (in_array($type, $inlineMimeTypeList)) {
|
if (in_array($type, $inlineMimeTypeList)) {
|
||||||
$disposition = 'inline';
|
$disposition = 'inline';
|
||||||
|
|
||||||
$response->setHeader('Content-Security-Policy', "default-src 'self'");
|
$response->setHeader('Content-Security-Policy', "default-src 'self'; script-src 'none'; object-src 'none';");
|
||||||
}
|
}
|
||||||
|
|
||||||
$response->setHeader('Content-Description', 'File Transfer');
|
$response->setHeader('Content-Description', 'File Transfer');
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class Image implements EntryPoint
|
|||||||
$response
|
$response
|
||||||
->setHeader('Content-Disposition', 'inline;filename="' . $fileName . '"')
|
->setHeader('Content-Disposition', 'inline;filename="' . $fileName . '"')
|
||||||
->setHeader('Content-Length', (string) $fileSize)
|
->setHeader('Content-Length', (string) $fileSize)
|
||||||
->setHeader('Content-Security-Policy', "default-src 'self'");
|
->setHeader('Content-Security-Policy', "default-src 'self'; script-src 'none'; object-src 'none';");
|
||||||
|
|
||||||
if (!$noCacheHeaders) {
|
if (!$noCacheHeaders) {
|
||||||
$response->setHeader('Cache-Control', 'private, max-age=864000, immutable');
|
$response->setHeader('Cache-Control', 'private, max-age=864000, immutable');
|
||||||
@@ -174,7 +174,9 @@ class Image implements EntryPoint
|
|||||||
|
|
||||||
$sourceId = $attachment->getSourceId();
|
$sourceId = $attachment->getSourceId();
|
||||||
|
|
||||||
$cacheFilePath = "data/upload/thumbs/{$sourceId}_$size";
|
$file = basename("{$sourceId}_$size");
|
||||||
|
|
||||||
|
$cacheFilePath = "data/upload/thumbs/$file";
|
||||||
|
|
||||||
if ($useCache && $this->fileManager->isFile($cacheFilePath)) {
|
if ($useCache && $this->fileManager->isFile($cacheFilePath)) {
|
||||||
return $this->fileManager->getContents($cacheFilePath);
|
return $this->fileManager->getContents($cacheFilePath);
|
||||||
|
|||||||
@@ -89,7 +89,9 @@ class RemoveFile implements AfterRemove
|
|||||||
$sizeList = array_keys($this->metadata->get(['app', 'image', 'sizes']) ?? []);
|
$sizeList = array_keys($this->metadata->get(['app', 'image', 'sizes']) ?? []);
|
||||||
|
|
||||||
foreach ($sizeList as $size) {
|
foreach ($sizeList as $size) {
|
||||||
$filePath = "data/upload/thumbs/{$entity->getSourceId()}_{$size}";
|
$file = basename("{$entity->getSourceId()}_$size");
|
||||||
|
|
||||||
|
$filePath = "data/upload/thumbs/$file";
|
||||||
|
|
||||||
if ($this->fileManager->isFile($filePath)) {
|
if ($this->fileManager->isFile($filePath)) {
|
||||||
$this->fileManager->removeFile($filePath);
|
$this->fileManager->removeFile($filePath);
|
||||||
|
|||||||
@@ -252,9 +252,9 @@
|
|||||||
"upgradeVersion": "EspoCRM bude upgradováno na verzi <strong>{version}</strong>. Toto může chvíli trvat.",
|
"upgradeVersion": "EspoCRM bude upgradováno na verzi <strong>{version}</strong>. Toto může chvíli trvat.",
|
||||||
"upgradeDone": "EspoCRM bylo upgradováno na verzi <strong>{version}</strong>.",
|
"upgradeDone": "EspoCRM bylo upgradováno na verzi <strong>{version}</strong>.",
|
||||||
"downloadUpgradePackage": "Stáhnout upgradovací balíčky na [tomto]({url}) odkaze.",
|
"downloadUpgradePackage": "Stáhnout upgradovací balíčky na [tomto]({url}) odkaze.",
|
||||||
"upgradeInfo": "Přečtěte si [dokumentaci]({url}) o tom, jak upgradovat instanci AutoCRM.",
|
"upgradeInfo": "Přečtěte si [dokumentaci]({url}) o tom, jak upgradovat instanci EspoCRM.",
|
||||||
"upgradeRecommendation": "Tento způsob upgradu se nedoporučuje. Je lepší upgradovat z CLI.",
|
"upgradeRecommendation": "Tento způsob upgradu se nedoporučuje. Je lepší upgradovat z CLI.",
|
||||||
"newVersionIsAvailable": "K dispozici je nová verze AutoCRM {latestVersion}. Při aktualizaci instance postupujte podle [pokynů](https://www.espocrm.com/documentation/administration/upgrading/).",
|
"newVersionIsAvailable": "K dispozici je nová verze EspoCRM {latestVersion}. Při aktualizaci instance postupujte podle [pokynů](https://www.espocrm.com/documentation/administration/upgrading/).",
|
||||||
"formulaFunctions": "Funkce formula skriptů",
|
"formulaFunctions": "Funkce formula skriptů",
|
||||||
"rebuildRequired": "Musíte spustit znovu rebuild z CLI.",
|
"rebuildRequired": "Musíte spustit znovu rebuild z CLI.",
|
||||||
"cronIsDisabled": "Cron je zakázán",
|
"cronIsDisabled": "Cron je zakázán",
|
||||||
@@ -293,7 +293,7 @@
|
|||||||
"authLog": "Historie přihlášení.",
|
"authLog": "Historie přihlášení.",
|
||||||
"attachments": "Všechny přílohy souborů uložené v systému.",
|
"attachments": "Všechny přílohy souborů uložené v systému.",
|
||||||
"templateManager": "Přizpůsobte si šablony zpráv.",
|
"templateManager": "Přizpůsobte si šablony zpráv.",
|
||||||
"systemRequirements": "Systémové požadavky na AutoCRM.",
|
"systemRequirements": "Systémové požadavky na EspoCRM.",
|
||||||
"apiUsers": "Oddělte uživatele pro účely integrace.",
|
"apiUsers": "Oddělte uživatele pro účely integrace.",
|
||||||
"jobs": "Spustit akce na pozadí.",
|
"jobs": "Spustit akce na pozadí.",
|
||||||
"pdfTemplates": "Šablony pro tisk do PDF.",
|
"pdfTemplates": "Šablony pro tisk do PDF.",
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
"Search": "Hledat",
|
"Search": "Hledat",
|
||||||
"Only My": "Pouze moje",
|
"Only My": "Pouze moje",
|
||||||
"Open": "Otevřený",
|
"Open": "Otevřený",
|
||||||
"About": "O AutoCRM",
|
"About": "O EspoCRM",
|
||||||
"Refresh": "Obnovit",
|
"Refresh": "Obnovit",
|
||||||
"Remove": "Odebrat",
|
"Remove": "Odebrat",
|
||||||
"Options": "Možnosti",
|
"Options": "Možnosti",
|
||||||
|
|||||||
@@ -186,18 +186,18 @@
|
|||||||
"ldapBaseDn": "Výchozí základní DN používané pro vyhledávání uživatelů. Např. \"OU = uživatelé, OU = espocrm, DC = test, DC = lan\".",
|
"ldapBaseDn": "Výchozí základní DN používané pro vyhledávání uživatelů. Např. \"OU = uživatelé, OU = espocrm, DC = test, DC = lan\".",
|
||||||
"ldapTryUsernameSplit": "Možnost rozdělit uživatelské jméno na doménu.",
|
"ldapTryUsernameSplit": "Možnost rozdělit uživatelské jméno na doménu.",
|
||||||
"ldapOptReferrals": "pokud by měla být sledována doporučení klientovi LDAP.",
|
"ldapOptReferrals": "pokud by měla být sledována doporučení klientovi LDAP.",
|
||||||
"ldapCreateEspoUser": "Tato možnost umožňuje AutoCRM vytvořit uživatele z LDAP.",
|
"ldapCreateEspoUser": "Tato možnost umožňuje EspoCRM vytvořit uživatele z LDAP.",
|
||||||
"ldapUserFirstNameAttribute": "Atribut LDAP, který se používá k určení křestního jména uživatele. Např. \"křestní jméno\".",
|
"ldapUserFirstNameAttribute": "Atribut LDAP, který se používá k určení křestního jména uživatele. Např. \"křestní jméno\".",
|
||||||
"ldapUserLastNameAttribute": "Atribut LDAP, který se používá k určení příjmení uživatele. Např. \"sn\".",
|
"ldapUserLastNameAttribute": "Atribut LDAP, který se používá k určení příjmení uživatele. Např. \"sn\".",
|
||||||
"ldapUserTitleAttribute": "LDAP atribut pro titul uživatele.",
|
"ldapUserTitleAttribute": "LDAP atribut pro titul uživatele.",
|
||||||
"ldapUserEmailAddressAttribute": "Atribut LDAP, který se používá k určení e-mailové adresy uživatele. Např. \"pošta\".",
|
"ldapUserEmailAddressAttribute": "Atribut LDAP, který se používá k určení e-mailové adresy uživatele. Např. \"pošta\".",
|
||||||
"ldapUserPhoneNumberAttribute": "LDAP atribut pro telefonní číslo uživatele.",
|
"ldapUserPhoneNumberAttribute": "LDAP atribut pro telefonní číslo uživatele.",
|
||||||
"ldapUserLoginFilter": "Filtr, který umožňuje omezit uživatele, kteří mohou používat AutoCRM. Např. \"memberOf = CN = espoGroup, OU = groups, OU = espocrm, DC = test, DC = lan\".",
|
"ldapUserLoginFilter": "Filtr, který umožňuje omezit uživatele, kteří mohou používat EspoCRM. Např. \"memberOf = CN = espoGroup, OU = groups, OU = espocrm, DC = test, DC = lan\".",
|
||||||
"ldapAccountDomainName": "Doména, která se používá k autorizaci k serveru LDAP.",
|
"ldapAccountDomainName": "Doména, která se používá k autorizaci k serveru LDAP.",
|
||||||
"ldapAccountDomainNameShort": "Krátká doména, která se používá k autorizaci k serveru LDAP.",
|
"ldapAccountDomainNameShort": "Krátká doména, která se používá k autorizaci k serveru LDAP.",
|
||||||
"ldapUserTeams": "LDAP týmy pro uživatele.",
|
"ldapUserTeams": "LDAP týmy pro uživatele.",
|
||||||
"ldapUserDefaultTeam": "Výchozí tým pro vytvořeného uživatele. Další informace najdete v uživatelském profilu.",
|
"ldapUserDefaultTeam": "Výchozí tým pro vytvořeného uživatele. Další informace najdete v uživatelském profilu.",
|
||||||
"b2cMode": "Ve výchozím nastavení je AutoCRM přizpůsoben pro B2B. Můžete jej přepnout na B2C.",
|
"b2cMode": "Ve výchozím nastavení je EspoCRM přizpůsoben pro B2B. Můžete jej přepnout na B2C.",
|
||||||
"aclStrictMode": "Povoleno: Přístup k rozsahům bude zakázán, pokud není uveden v rolích. \nZakázán: Přístup k rozsahům bude povolen, pokud není uveden v rolích.",
|
"aclStrictMode": "Povoleno: Přístup k rozsahům bude zakázán, pokud není uveden v rolích. \nZakázán: Přístup k rozsahům bude povolen, pokud není uveden v rolích.",
|
||||||
"outboundEmailIsShared": "Povolit posílání emailů uživatelům pomocí SMTP.",
|
"outboundEmailIsShared": "Povolit posílání emailů uživatelům pomocí SMTP.",
|
||||||
"streamEmailNotificationsEntityList": "Emailová upozornění na aktualizace streamu sledovaných záznamů. Uživatelé budou dostávat e-mailová oznámení pouze pro určené typy entit.",
|
"streamEmailNotificationsEntityList": "Emailová upozornění na aktualizace streamu sledovaných záznamů. Uživatelé budou dostávat e-mailová oznámení pouze pro určené typy entit.",
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class UploadUrlService
|
|||||||
*/
|
*/
|
||||||
public function uploadImage(string $url, FieldData $data): Attachment
|
public function uploadImage(string $url, FieldData $data): Attachment
|
||||||
{
|
{
|
||||||
if (!$this->urlCheck->isNotInternalUrl($url)) {
|
if (!$this->urlCheck->isUrlAndNotIternal($url)) {
|
||||||
throw new ForbiddenSilent("Not allowed URL.");
|
throw new ForbiddenSilent("Not allowed URL.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,9 +114,20 @@ class UploadUrlService
|
|||||||
/**
|
/**
|
||||||
* @param non-empty-string $url
|
* @param non-empty-string $url
|
||||||
* @return ?array{string, string} A type and contents.
|
* @return ?array{string, string} A type and contents.
|
||||||
|
* @throws ForbiddenSilent
|
||||||
*/
|
*/
|
||||||
private function getImageDataByUrl(string $url): ?array
|
private function getImageDataByUrl(string $url): ?array
|
||||||
{
|
{
|
||||||
|
$resolve = $this->urlCheck->getCurlResolve($url);
|
||||||
|
|
||||||
|
if ($resolve === []) {
|
||||||
|
throw new ForbiddenSilent("Could not resolve the host.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($resolve !== null && !$this->urlCheck->validateCurlResolveNotInternal($resolve)) {
|
||||||
|
throw new ForbiddenSilent("Forbidden host.");
|
||||||
|
}
|
||||||
|
|
||||||
$type = null;
|
$type = null;
|
||||||
|
|
||||||
if (!function_exists('curl_init')) {
|
if (!function_exists('curl_init')) {
|
||||||
@@ -144,6 +155,10 @@ class UploadUrlService
|
|||||||
$opts[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTPS | \CURLPROTO_HTTP;
|
$opts[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTPS | \CURLPROTO_HTTP;
|
||||||
$opts[\CURLOPT_REDIR_PROTOCOLS] = \CURLPROTO_HTTPS;
|
$opts[\CURLOPT_REDIR_PROTOCOLS] = \CURLPROTO_HTTPS;
|
||||||
|
|
||||||
|
if ($resolve) {
|
||||||
|
$opts[CURLOPT_RESOLVE] = $resolve;
|
||||||
|
}
|
||||||
|
|
||||||
$ch = curl_init();
|
$ch = curl_init();
|
||||||
|
|
||||||
curl_setopt_array($ch, $opts);
|
curl_setopt_array($ch, $opts);
|
||||||
|
|||||||
@@ -36,8 +36,11 @@ use Espo\Core\Api\Response;
|
|||||||
use Espo\Core\Api\ResponseComposer;
|
use Espo\Core\Api\ResponseComposer;
|
||||||
use Espo\Core\Exceptions\BadRequest;
|
use Espo\Core\Exceptions\BadRequest;
|
||||||
use Espo\Core\Exceptions\Forbidden;
|
use Espo\Core\Exceptions\Forbidden;
|
||||||
|
use Espo\Core\Exceptions\NotFound;
|
||||||
|
use Espo\Entities\Attachment;
|
||||||
use Espo\Entities\Email;
|
use Espo\Entities\Email;
|
||||||
use Espo\Entities\User;
|
use Espo\Entities\User;
|
||||||
|
use Espo\ORM\EntityManager;
|
||||||
use Espo\Tools\Email\ImportEmlService;
|
use Espo\Tools\Email\ImportEmlService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,6 +52,7 @@ class PostImportEml implements Action
|
|||||||
private Acl $acl,
|
private Acl $acl,
|
||||||
private User $user,
|
private User $user,
|
||||||
private ImportEmlService $service,
|
private ImportEmlService $service,
|
||||||
|
private EntityManager $entityManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function process(Request $request): Response
|
public function process(Request $request): Response
|
||||||
@@ -61,11 +65,32 @@ class PostImportEml implements Action
|
|||||||
throw new BadRequest("No 'fileId'.");
|
throw new BadRequest("No 'fileId'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$email = $this->service->import($fileId, $this->user->getId());
|
$attachment = $this->getAttachment($fileId);
|
||||||
|
|
||||||
|
$email = $this->service->import($attachment, $this->user->getId());
|
||||||
|
|
||||||
return ResponseComposer::json(['id' => $email->getId()]);
|
return ResponseComposer::json(['id' => $email->getId()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NotFound
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
private function getAttachment(string $fileId): Attachment
|
||||||
|
{
|
||||||
|
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getById($fileId);
|
||||||
|
|
||||||
|
if (!$attachment) {
|
||||||
|
throw new NotFound("Attachment not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->acl->checkEntityRead($attachment)) {
|
||||||
|
throw new Forbidden("No access to attachment.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $attachment;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws Forbidden
|
* @throws Forbidden
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class PostSendTest implements Action
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!$this->addressUtil->isAllowedAddress($smtpParams) &&
|
!$this->addressUtil->isAllowedAddress($smtpParams) &&
|
||||||
!$this->hostCheck->isNotInternalHost($server)
|
!$this->hostCheck->isHostAndNotInternal($server)
|
||||||
) {
|
) {
|
||||||
throw new Forbidden("Not allowed internal host.");
|
throw new Forbidden("Not allowed internal host.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ namespace Espo\Tools\Email;
|
|||||||
|
|
||||||
use Espo\Core\Exceptions\Conflict;
|
use Espo\Core\Exceptions\Conflict;
|
||||||
use Espo\Core\Exceptions\Error;
|
use Espo\Core\Exceptions\Error;
|
||||||
use Espo\Core\Exceptions\NotFound;
|
|
||||||
use Espo\Core\FileStorage\Manager;
|
use Espo\Core\FileStorage\Manager;
|
||||||
use Espo\Core\Mail\Exceptions\ImapError;
|
use Espo\Core\Mail\Exceptions\ImapError;
|
||||||
use Espo\Core\Mail\Importer;
|
use Espo\Core\Mail\Importer;
|
||||||
@@ -56,16 +55,13 @@ class ImportEmlService
|
|||||||
/**
|
/**
|
||||||
* Import an EML.
|
* Import an EML.
|
||||||
*
|
*
|
||||||
* @param string $fileId An attachment ID.
|
|
||||||
* @param ?string $userId A user ID to relate an email with.
|
* @param ?string $userId A user ID to relate an email with.
|
||||||
* @return Email An Email.
|
* @return Email An Email.
|
||||||
* @throws NotFound
|
|
||||||
* @throws Error
|
* @throws Error
|
||||||
* @throws Conflict
|
* @throws Conflict
|
||||||
*/
|
*/
|
||||||
public function import(string $fileId, ?string $userId = null): Email
|
public function import(Attachment $attachment, ?string $userId = null): Email
|
||||||
{
|
{
|
||||||
$attachment = $this->getAttachment($fileId);
|
|
||||||
$contents = $this->fileStorageManager->getContents($attachment);
|
$contents = $this->fileStorageManager->getContents($attachment);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -93,20 +89,6 @@ class ImportEmlService
|
|||||||
return $email;
|
return $email;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws NotFound
|
|
||||||
*/
|
|
||||||
private function getAttachment(string $fileId): Attachment
|
|
||||||
{
|
|
||||||
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getById($fileId);
|
|
||||||
|
|
||||||
if (!$attachment) {
|
|
||||||
throw new NotFound("Attachment not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return $attachment;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws Conflict
|
* @throws Conflict
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ use Espo\Core\Notification\EmailNotificationHandler;
|
|||||||
use Espo\Core\Mail\SenderParams;
|
use Espo\Core\Mail\SenderParams;
|
||||||
use Espo\Core\Utils\Config\ApplicationConfig;
|
use Espo\Core\Utils\Config\ApplicationConfig;
|
||||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||||
|
use Espo\Core\Utils\Markdown\Markdown;
|
||||||
use Espo\Entities\Note;
|
use Espo\Entities\Note;
|
||||||
use Espo\ORM\Collection;
|
use Espo\ORM\Collection;
|
||||||
use Espo\Repositories\Portal as PortalRepository;
|
use Espo\Repositories\Portal as PortalRepository;
|
||||||
@@ -58,8 +59,6 @@ use Espo\Core\Utils\TemplateFileManager;
|
|||||||
use Espo\Core\Utils\Util;
|
use Espo\Core\Utils\Util;
|
||||||
use Espo\Tools\Stream\NoteAccessControl;
|
use Espo\Tools\Stream\NoteAccessControl;
|
||||||
|
|
||||||
use Michelf\Markdown;
|
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
@@ -325,11 +324,10 @@ class Processor
|
|||||||
|
|
||||||
$data['userName'] = $note->get('createdByName');
|
$data['userName'] = $note->get('createdByName');
|
||||||
|
|
||||||
$post = Markdown::defaultTransform(
|
$post = $note->getPost() ?? '';
|
||||||
$note->get('post') ?? ''
|
|
||||||
);
|
|
||||||
|
|
||||||
$data['post'] = $post;
|
|
||||||
|
$data['post'] = Markdown::transform($post);
|
||||||
|
|
||||||
$subjectTpl = $this->templateFileManager->getTemplate('mention', 'subject');
|
$subjectTpl = $this->templateFileManager->getTemplate('mention', 'subject');
|
||||||
$bodyTpl = $this->templateFileManager->getTemplate('mention', 'body');
|
$bodyTpl = $this->templateFileManager->getTemplate('mention', 'body');
|
||||||
@@ -486,9 +484,7 @@ class Processor
|
|||||||
|
|
||||||
$data['userName'] = $note->get('createdByName');
|
$data['userName'] = $note->get('createdByName');
|
||||||
|
|
||||||
$post = Markdown::defaultTransform($note->getPost() ?? '');
|
$data['post'] = Markdown::transform($note->getPost() ?? '');
|
||||||
|
|
||||||
$data['post'] = $post;
|
|
||||||
|
|
||||||
$parent = null;
|
$parent = null;
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
|||||||
/*! espocrm 2026-03-10 */
|
/*! espocrm 2026-03-24 */
|
||||||
define("modules/crm/views/scheduler/scheduler",["exports","view","vis-data","vis-timeline","moment","jquery"],function(t,e,a,s,n,r){Object.defineProperty(t,"__esModule",{value:!0});t.default=void 0;e=i(e);n=i(n);r=i(r);function i(t){return t&&t.__esModule?t:{default:t}}class o extends e.default{templateContent=`
|
define("modules/crm/views/scheduler/scheduler",["exports","view","vis-data","vis-timeline","moment","jquery"],function(t,e,a,s,n,r){Object.defineProperty(t,"__esModule",{value:!0});t.default=void 0;e=i(e);n=i(n);r=i(r);function i(t){return t&&t.__esModule?t:{default:t}}class o extends e.default{templateContent=`
|
||||||
<div class="timeline"></div>
|
<div class="timeline"></div>
|
||||||
<link href="{{basePath}}client/modules/crm/css/vis.css" rel="stylesheet">
|
<link href="{{basePath}}client/modules/crm/css/vis.css" rel="stylesheet">
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
71
custom/Espo/Custom/Api/JunctionData/GetDokumentes.php
Normal file
71
custom/Espo/Custom/Api/JunctionData/GetDokumentes.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Espo\Custom\Api\JunctionData;
|
||||||
|
|
||||||
|
use Espo\Core\Api\Action;
|
||||||
|
use Espo\Core\Api\Request;
|
||||||
|
use Espo\Core\Api\Response;
|
||||||
|
use Espo\Core\Api\ResponseComposer;
|
||||||
|
use Espo\Core\Exceptions\BadRequest;
|
||||||
|
use Espo\Core\Exceptions\NotFound;
|
||||||
|
use Espo\ORM\EntityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/JunctionData/CAIKnowledge/:knowledgeId/dokumentes
|
||||||
|
*
|
||||||
|
* Returns all documents linked to a knowledge entry with junction table data
|
||||||
|
*/
|
||||||
|
class GetDokumentes implements Action
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Request $request): Response
|
||||||
|
{
|
||||||
|
$knowledgeId = $request->getRouteParam('knowledgeId');
|
||||||
|
|
||||||
|
if (!$knowledgeId) {
|
||||||
|
throw new BadRequest('Knowledge ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify knowledge exists
|
||||||
|
$knowledge = $this->entityManager->getEntityById('CAIKnowledge', $knowledgeId);
|
||||||
|
if (!$knowledge) {
|
||||||
|
throw new NotFound('Knowledge entry not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = $this->entityManager->getPDO();
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
j.id as junctionId,
|
||||||
|
j.c_a_i_knowledge_id as cAIKnowledgeId,
|
||||||
|
j.c_dokumente_id as cDokumenteId,
|
||||||
|
j.ai_document_id as aiDocumentId,
|
||||||
|
j.syncstatus,
|
||||||
|
j.last_sync as lastSync,
|
||||||
|
d.id as documentId,
|
||||||
|
d.name as documentName,
|
||||||
|
d.blake3hash as blake3hash,
|
||||||
|
d.created_at as documentCreatedAt,
|
||||||
|
d.modified_at as documentModifiedAt
|
||||||
|
FROM c_a_i_knowledge_dokumente j
|
||||||
|
INNER JOIN c_dokumente d ON j.c_dokumente_id = d.id
|
||||||
|
WHERE j.c_a_i_knowledge_id = :knowledgeId
|
||||||
|
AND j.deleted = 0
|
||||||
|
AND d.deleted = 0
|
||||||
|
ORDER BY j.id DESC
|
||||||
|
";
|
||||||
|
|
||||||
|
$sth = $pdo->prepare($sql);
|
||||||
|
$sth->execute(['knowledgeId' => $knowledgeId]);
|
||||||
|
|
||||||
|
$results = $sth->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return ResponseComposer::json([
|
||||||
|
'total' => count($results),
|
||||||
|
'list' => $results
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
178
custom/Espo/Custom/Api/JunctionData/LinkDokument.php
Normal file
178
custom/Espo/Custom/Api/JunctionData/LinkDokument.php
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Espo\Custom\Api\JunctionData;
|
||||||
|
|
||||||
|
use Espo\Core\Api\Action;
|
||||||
|
use Espo\Core\Api\Request;
|
||||||
|
use Espo\Core\Api\Response;
|
||||||
|
use Espo\Core\Api\ResponseComposer;
|
||||||
|
use Espo\Core\Exceptions\BadRequest;
|
||||||
|
use Espo\Core\Exceptions\NotFound;
|
||||||
|
use Espo\Core\Exceptions\Conflict;
|
||||||
|
use Espo\ORM\EntityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/v1/JunctionData/CAIKnowledge/:knowledgeId/dokumentes/:documentId
|
||||||
|
*
|
||||||
|
* Creates or updates relationship with junction table data
|
||||||
|
* This endpoint links the entities AND sets junction columns in one call
|
||||||
|
*/
|
||||||
|
class LinkDokument implements Action
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Request $request): Response
|
||||||
|
{
|
||||||
|
$knowledgeId = $request->getRouteParam('knowledgeId');
|
||||||
|
$documentId = $request->getRouteParam('documentId');
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
|
||||||
|
if (!$knowledgeId || !$documentId) {
|
||||||
|
throw new BadRequest('Knowledge ID and Document ID are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify entities exist
|
||||||
|
$knowledge = $this->entityManager->getEntityById('CAIKnowledge', $knowledgeId);
|
||||||
|
if (!$knowledge) {
|
||||||
|
throw new NotFound('Knowledge entry not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$document = $this->entityManager->getEntityById('CDokumente', $documentId);
|
||||||
|
if (!$document) {
|
||||||
|
throw new NotFound('Document not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = $this->entityManager->getPDO();
|
||||||
|
|
||||||
|
// Check if link already exists
|
||||||
|
$existing = $this->checkIfLinked($knowledgeId, $documentId);
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
// Link exists - update junction columns
|
||||||
|
return $this->updateExisting($knowledgeId, $documentId, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new link via ORM (triggers hooks like DokumenteSyncStatus)
|
||||||
|
$this->entityManager->getRDBRepository('CAIKnowledge')
|
||||||
|
->getRelation($knowledge, 'dokumentes')
|
||||||
|
->relate($document);
|
||||||
|
|
||||||
|
// Now set junction columns if provided
|
||||||
|
if (!empty((array)$data)) {
|
||||||
|
return $this->updateExisting($knowledgeId, $documentId, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return created entry
|
||||||
|
$result = $this->getJunctionEntry($knowledgeId, $documentId);
|
||||||
|
|
||||||
|
return ResponseComposer::json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkIfLinked(string $knowledgeId, string $documentId): bool
|
||||||
|
{
|
||||||
|
$pdo = $this->entityManager->getPDO();
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM c_a_i_knowledge_dokumente
|
||||||
|
WHERE c_a_i_knowledge_id = :knowledgeId
|
||||||
|
AND c_dokumente_id = :documentId
|
||||||
|
AND deleted = 0
|
||||||
|
";
|
||||||
|
|
||||||
|
$sth = $pdo->prepare($sql);
|
||||||
|
$sth->execute([
|
||||||
|
'knowledgeId' => $knowledgeId,
|
||||||
|
'documentId' => $documentId
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $sth->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
return $result['count'] > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateExisting(string $knowledgeId, string $documentId, \stdClass $data): Response
|
||||||
|
{
|
||||||
|
$pdo = $this->entityManager->getPDO();
|
||||||
|
|
||||||
|
// Build dynamic UPDATE SET clause
|
||||||
|
$setClauses = [];
|
||||||
|
$params = [
|
||||||
|
'knowledgeId' => $knowledgeId,
|
||||||
|
'documentId' => $documentId
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($data->aiDocumentId)) {
|
||||||
|
$setClauses[] = "ai_document_id = :aiDocumentId";
|
||||||
|
$params['aiDocumentId'] = $data->aiDocumentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data->syncstatus)) {
|
||||||
|
$allowedStatuses = ['new', 'unclean', 'synced', 'failed', 'unsupported'];
|
||||||
|
if (!in_array($data->syncstatus, $allowedStatuses)) {
|
||||||
|
throw new BadRequest('Invalid syncstatus value. Allowed: ' . implode(', ', $allowedStatuses));
|
||||||
|
}
|
||||||
|
$setClauses[] = "syncstatus = :syncstatus";
|
||||||
|
$params['syncstatus'] = $data->syncstatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data->lastSync)) {
|
||||||
|
$setClauses[] = "last_sync = :lastSync";
|
||||||
|
$params['lastSync'] = $data->lastSync;
|
||||||
|
} elseif (isset($data->updateLastSync) && $data->updateLastSync === true) {
|
||||||
|
$setClauses[] = "last_sync = NOW()";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($setClauses)) {
|
||||||
|
$sql = "
|
||||||
|
UPDATE c_a_i_knowledge_dokumente
|
||||||
|
SET " . implode(', ', $setClauses) . "
|
||||||
|
WHERE c_a_i_knowledge_id = :knowledgeId
|
||||||
|
AND c_dokumente_id = :documentId
|
||||||
|
AND deleted = 0
|
||||||
|
";
|
||||||
|
|
||||||
|
$sth = $pdo->prepare($sql);
|
||||||
|
$sth->execute($params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return updated data
|
||||||
|
$result = $this->getJunctionEntry($knowledgeId, $documentId);
|
||||||
|
|
||||||
|
return ResponseComposer::json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getJunctionEntry(string $knowledgeId, string $documentId): array
|
||||||
|
{
|
||||||
|
$pdo = $this->entityManager->getPDO();
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
id as junctionId,
|
||||||
|
c_a_i_knowledge_id as cAIKnowledgeId,
|
||||||
|
c_dokumente_id as cDokumenteId,
|
||||||
|
ai_document_id as aiDocumentId,
|
||||||
|
syncstatus,
|
||||||
|
last_sync as lastSync
|
||||||
|
FROM c_a_i_knowledge_dokumente
|
||||||
|
WHERE c_a_i_knowledge_id = :knowledgeId
|
||||||
|
AND c_dokumente_id = :documentId
|
||||||
|
AND deleted = 0
|
||||||
|
";
|
||||||
|
|
||||||
|
$sth = $pdo->prepare($sql);
|
||||||
|
$sth->execute([
|
||||||
|
'knowledgeId' => $knowledgeId,
|
||||||
|
'documentId' => $documentId
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $sth->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
throw new NotFound('Junction entry not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
123
custom/Espo/Custom/Api/JunctionData/UpdateJunction.php
Normal file
123
custom/Espo/Custom/Api/JunctionData/UpdateJunction.php
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Espo\Custom\Api\JunctionData;
|
||||||
|
|
||||||
|
use Espo\Core\Api\Action;
|
||||||
|
use Espo\Core\Api\Request;
|
||||||
|
use Espo\Core\Api\Response;
|
||||||
|
use Espo\Core\Api\ResponseComposer;
|
||||||
|
use Espo\Core\Exceptions\BadRequest;
|
||||||
|
use Espo\Core\Exceptions\NotFound;
|
||||||
|
use Espo\ORM\EntityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/v1/JunctionData/CAIKnowledge/:knowledgeId/dokumentes/:documentId
|
||||||
|
*
|
||||||
|
* Updates junction table columns for an existing relationship
|
||||||
|
*/
|
||||||
|
class UpdateJunction implements Action
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Request $request): Response
|
||||||
|
{
|
||||||
|
$knowledgeId = $request->getRouteParam('knowledgeId');
|
||||||
|
$documentId = $request->getRouteParam('documentId');
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
|
||||||
|
if (!$knowledgeId || !$documentId) {
|
||||||
|
throw new BadRequest('Knowledge ID and Document ID are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = $this->entityManager->getPDO();
|
||||||
|
|
||||||
|
// Build dynamic UPDATE SET clause
|
||||||
|
$setClauses = [];
|
||||||
|
$params = [
|
||||||
|
'knowledgeId' => $knowledgeId,
|
||||||
|
'documentId' => $documentId
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($data->aiDocumentId)) {
|
||||||
|
$setClauses[] = "ai_document_id = :aiDocumentId";
|
||||||
|
$params['aiDocumentId'] = $data->aiDocumentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data->syncstatus)) {
|
||||||
|
$allowedStatuses = ['new', 'unclean', 'synced', 'failed', 'unsupported'];
|
||||||
|
if (!in_array($data->syncstatus, $allowedStatuses)) {
|
||||||
|
throw new BadRequest('Invalid syncstatus value. Allowed: ' . implode(', ', $allowedStatuses));
|
||||||
|
}
|
||||||
|
$setClauses[] = "syncstatus = :syncstatus";
|
||||||
|
$params['syncstatus'] = $data->syncstatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data->lastSync)) {
|
||||||
|
$setClauses[] = "last_sync = :lastSync";
|
||||||
|
$params['lastSync'] = $data->lastSync;
|
||||||
|
} elseif (isset($data->updateLastSync) && $data->updateLastSync === true) {
|
||||||
|
$setClauses[] = "last_sync = NOW()";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($setClauses)) {
|
||||||
|
throw new BadRequest('No fields to update. Provide at least one of: aiDocumentId, syncstatus, lastSync');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
UPDATE c_a_i_knowledge_dokumente
|
||||||
|
SET " . implode(', ', $setClauses) . "
|
||||||
|
WHERE c_a_i_knowledge_id = :knowledgeId
|
||||||
|
AND c_dokumente_id = :documentId
|
||||||
|
AND deleted = 0
|
||||||
|
";
|
||||||
|
|
||||||
|
$sth = $pdo->prepare($sql);
|
||||||
|
$sth->execute($params);
|
||||||
|
|
||||||
|
$affectedRows = $sth->rowCount();
|
||||||
|
|
||||||
|
if ($affectedRows === 0) {
|
||||||
|
throw new NotFound('Junction entry not found or no changes made');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return updated data
|
||||||
|
$result = $this->getJunctionEntry($knowledgeId, $documentId);
|
||||||
|
|
||||||
|
return ResponseComposer::json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getJunctionEntry(string $knowledgeId, string $documentId): array
|
||||||
|
{
|
||||||
|
$pdo = $this->entityManager->getPDO();
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
id as junctionId,
|
||||||
|
c_a_i_knowledge_id as cAIKnowledgeId,
|
||||||
|
c_dokumente_id as cDokumenteId,
|
||||||
|
ai_document_id as aiDocumentId,
|
||||||
|
syncstatus,
|
||||||
|
last_sync as lastSync
|
||||||
|
FROM c_a_i_knowledge_dokumente
|
||||||
|
WHERE c_a_i_knowledge_id = :knowledgeId
|
||||||
|
AND c_dokumente_id = :documentId
|
||||||
|
AND deleted = 0
|
||||||
|
";
|
||||||
|
|
||||||
|
$sth = $pdo->prepare($sql);
|
||||||
|
$sth->execute([
|
||||||
|
'knowledgeId' => $knowledgeId,
|
||||||
|
'documentId' => $documentId
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $sth->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
throw new NotFound('Junction entry not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Espo\Custom\Controllers;
|
|
||||||
|
|
||||||
class CAICollection extends \Espo\Core\Templates\Controllers\BasePlus
|
|
||||||
{
|
|
||||||
}
|
|
||||||
7
custom/Espo/Custom/Controllers/CAIKnowledge.php
Normal file
7
custom/Espo/Custom/Controllers/CAIKnowledge.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Espo\Custom\Controllers;
|
||||||
|
|
||||||
|
class CAIKnowledge extends \Espo\Core\Templates\Controllers\Base
|
||||||
|
{
|
||||||
|
}
|
||||||
24
custom/Espo/Custom/Controllers/CAIKnowledgeCDokumente.php
Normal file
24
custom/Espo/Custom/Controllers/CAIKnowledgeCDokumente.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Controllers;
|
||||||
|
|
||||||
|
use Espo\Core\Controllers\Record;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Junction Controller: CAIKnowledge ↔ CDokumente
|
||||||
|
*
|
||||||
|
* Provides REST API access to the junction table with additionalColumns:
|
||||||
|
* - aiDocumentId: External AI document reference
|
||||||
|
* - syncstatus: Sync state tracking (new, unclean, synced, failed)
|
||||||
|
* - lastSync: Last synchronization timestamp
|
||||||
|
*/
|
||||||
|
class CAIKnowledgeCDokumente extends Record
|
||||||
|
{
|
||||||
|
// Inherits all CRUD operations from Record controller
|
||||||
|
//
|
||||||
|
// Available endpoints:
|
||||||
|
// GET /api/v1/CAIKnowledgeCDokumente
|
||||||
|
// GET /api/v1/CAIKnowledgeCDokumente/{id}
|
||||||
|
// POST /api/v1/CAIKnowledgeCDokumente
|
||||||
|
// PUT /api/v1/CAIKnowledgeCDokumente/{id}
|
||||||
|
// DELETE /api/v1/CAIKnowledgeCDokumente/{id}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CAIKnowledge;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\ORM\Repository\Option\SaveOptions;
|
||||||
|
use Espo\Core\Hook\Hook\BeforeSave;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Prüft Junction-Table und aktualisiert globalen syncStatus
|
||||||
|
* basierend auf den syncstatus-Werten der verknüpften Dokumente
|
||||||
|
*/
|
||||||
|
class CheckGlobalSyncStatus implements BeforeSave
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function beforeSave(Entity $entity, SaveOptions $options): void
|
||||||
|
{
|
||||||
|
// Überspringe, wenn skipHooks gesetzt ist (verhindert Loops)
|
||||||
|
if ($options->get('skipHooks')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur wenn Entity bereits existiert (nicht bei Create)
|
||||||
|
if ($entity->isNew()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hole alle verknüpften Dokumente mit ihren syncstatus-Werten aus der Junction-Tabelle
|
||||||
|
$query = $this->entityManager->getQueryBuilder()
|
||||||
|
->select(['syncstatus'])
|
||||||
|
->from('CAIKnowledgeDokumente')
|
||||||
|
->where([
|
||||||
|
'cAIKnowledgeId' => $entity->getId(),
|
||||||
|
'deleted' => false
|
||||||
|
])
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$pdoStatement = $this->entityManager->getQueryExecutor()->execute($query);
|
||||||
|
$rows = $pdoStatement->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Wenn keine Dokumente verknüpft, setze auf "unclean"
|
||||||
|
if (empty($rows)) {
|
||||||
|
$entity->set('syncStatus', 'unclean');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe, ob irgendein Dokument "new" oder "unclean" ist
|
||||||
|
$hasUnsynced = false;
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$status = $row['syncstatus'] ?? null;
|
||||||
|
if ($status === 'new' || $status === 'unclean' || $status === null || $status === '') {
|
||||||
|
$hasUnsynced = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setze globalen Status
|
||||||
|
if ($hasUnsynced) {
|
||||||
|
$entity->set('syncStatus', 'unclean');
|
||||||
|
} else {
|
||||||
|
// Alle Dokumente sind "synced"
|
||||||
|
$entity->set('syncStatus', 'synced');
|
||||||
|
$entity->set('lastSync', date('Y-m-d H:i:s'));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Bei Fehler loggen und Status auf "unclean" setzen
|
||||||
|
$GLOBALS['log']->error('CAIKnowledge CheckGlobalSyncStatus Hook Error: ' . $e->getMessage());
|
||||||
|
$entity->set('syncStatus', 'unclean');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CAIKnowledge;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Core\Hook\Hook\AfterRelate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Setzt Dokument-Sync-Status auf "new" beim Verknüpfen und
|
||||||
|
* globalen syncStatus auf "unclean"
|
||||||
|
*/
|
||||||
|
class DokumenteSyncStatus implements AfterRelate
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterRelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
array $columnData,
|
||||||
|
\Espo\ORM\Repository\Option\RelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für dokumentes-Beziehung
|
||||||
|
if ($relationName !== 'dokumentes') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setze Sync-Status des Dokuments in der Junction-Tabelle auf "new"
|
||||||
|
$repository = $this->entityManager->getRDBRepository('CAIKnowledge');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$repository->getRelation($entity, 'dokumentes')->updateColumns(
|
||||||
|
$foreignEntity,
|
||||||
|
['syncstatus' => 'new']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Setze globalen syncStatus auf "unclean"
|
||||||
|
$entity->set('syncStatus', 'unclean');
|
||||||
|
$this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Fehler loggen, aber nicht werfen (um Verknüpfung nicht zu blockieren)
|
||||||
|
$GLOBALS['log']->error('CAIKnowledge DokumenteSyncStatus Hook Error: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
182
custom/Espo/Custom/Hooks/CAIKnowledge/PropagateDocumentsUp.php
Normal file
182
custom/Espo/Custom/Hooks/CAIKnowledge/PropagateDocumentsUp.php
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CAIKnowledge;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Core\Hook\Hook\AfterRelate;
|
||||||
|
use Espo\Core\Hook\Hook\AfterUnrelate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Propagiert Dokumenten-Verknüpfungen von AIKnowledge nach oben zu Räumungsklage/Mietinkasso
|
||||||
|
*
|
||||||
|
* Wenn Dokument mit AIKnowledge verknüpft wird:
|
||||||
|
* → verknüpfe mit verbundener Räumungsklage/Mietinkasso
|
||||||
|
* → von dort propagiert es automatisch zu AdvowareAkten (via deren Hooks)
|
||||||
|
*
|
||||||
|
* Wenn Dokument von AIKnowledge entknüpft wird:
|
||||||
|
* → entknüpfe von verbundener Räumungsklage/Mietinkasso
|
||||||
|
* → von dort propagiert es automatisch von AdvowareAkten (via deren Hooks)
|
||||||
|
*/
|
||||||
|
class PropagateDocumentsUp implements AfterRelate, AfterUnrelate
|
||||||
|
{
|
||||||
|
private static array $processing = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterRelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
array $columnData,
|
||||||
|
\Espo\ORM\Repository\Option\RelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für dokumentes-Beziehung
|
||||||
|
if ($relationName !== 'dokumentes') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-' . $foreignEntity->getId() . '-relate';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prüfe ob Räumungsklage verknüpft ist
|
||||||
|
$raumungsklage = $this->entityManager
|
||||||
|
->getRDBRepository('CAIKnowledge')
|
||||||
|
->getRelation($entity, 'vmhRumungsklage')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($raumungsklage) {
|
||||||
|
$this->relateDocument($raumungsklage, 'dokumentesvmhraumungsklage', $foreignEntity);
|
||||||
|
|
||||||
|
// Also link to AdvowareAkte if Räumungsklage has one
|
||||||
|
$advowareAkte = $this->entityManager
|
||||||
|
->getRDBRepository('CVmhRumungsklage')
|
||||||
|
->getRelation($raumungsklage, 'advowareAkten')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($advowareAkte && !$foreignEntity->get('cAdvowareAktenId')) {
|
||||||
|
$foreignEntity->set('cAdvowareAktenId', $advowareAkte->getId());
|
||||||
|
$foreignEntity->set('syncStatus', 'new');
|
||||||
|
$this->entityManager->saveEntity($foreignEntity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob Mietinkasso verknüpft ist
|
||||||
|
$mietinkasso = $this->entityManager
|
||||||
|
->getRDBRepository('CAIKnowledge')
|
||||||
|
->getRelation($entity, 'mietinkasso')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($mietinkasso) {
|
||||||
|
$this->relateDocument($mietinkasso, 'dokumentesmietinkasso', $foreignEntity);
|
||||||
|
|
||||||
|
// Also link to AdvowareAkte if Mietinkasso has one
|
||||||
|
$advowareAkte = $this->entityManager
|
||||||
|
->getRDBRepository('CMietinkasso')
|
||||||
|
->getRelation($mietinkasso, 'advowareAkten')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($advowareAkte && !$foreignEntity->get('cAdvowareAktenId')) {
|
||||||
|
$foreignEntity->set('cAdvowareAktenId', $advowareAkte->getId());
|
||||||
|
$foreignEntity->set('syncStatus', 'new');
|
||||||
|
$this->entityManager->saveEntity($foreignEntity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CAIKnowledge PropagateDocumentsUp (relate) Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function afterUnrelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
\Espo\ORM\Repository\Option\UnrelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für dokumentes-Beziehung
|
||||||
|
if ($relationName !== 'dokumentes') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-' . $foreignEntity->getId() . '-unrelate';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prüfe ob Räumungsklage verknüpft ist
|
||||||
|
$raumungsklage = $this->entityManager
|
||||||
|
->getRDBRepository('CAIKnowledge')
|
||||||
|
->getRelation($entity, 'vmhRumungsklage')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($raumungsklage) {
|
||||||
|
$this->unrelateDocument($raumungsklage, 'dokumentesvmhraumungsklage', $foreignEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob Mietinkasso verknüpft ist
|
||||||
|
$mietinkasso = $this->entityManager
|
||||||
|
->getRDBRepository('CAIKnowledge')
|
||||||
|
->getRelation($entity, 'mietinkasso')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($mietinkasso) {
|
||||||
|
$this->unrelateDocument($mietinkasso, 'dokumentesmietinkasso', $foreignEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: We don't remove cAdvowareAktenId on unrelate from AIKnowledge
|
||||||
|
// because the document might still be linked to Räumungsklage/Mietinkasso
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CAIKnowledge PropagateDocumentsUp (unrelate) Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion: Verknüpfe Dokument (nur wenn nicht bereits verknüpft)
|
||||||
|
*/
|
||||||
|
private function relateDocument(Entity $parentEntity, string $relationName, Entity $document): void
|
||||||
|
{
|
||||||
|
$repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType());
|
||||||
|
$relation = $repository->getRelation($parentEntity, $relationName);
|
||||||
|
|
||||||
|
// Prüfe ob bereits verknüpft
|
||||||
|
$isRelated = $relation
|
||||||
|
->where(['id' => $document->getId()])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if (!$isRelated) {
|
||||||
|
$relation->relate($document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion: Entknüpfe Dokument
|
||||||
|
*/
|
||||||
|
private function unrelateDocument(Entity $parentEntity, string $relationName, Entity $document): void
|
||||||
|
{
|
||||||
|
$repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType());
|
||||||
|
$relation = $repository->getRelation($parentEntity, $relationName);
|
||||||
|
|
||||||
|
// Prüfe ob verknüpft
|
||||||
|
$isRelated = $relation
|
||||||
|
->where(['id' => $document->getId()])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($isRelated) {
|
||||||
|
$relation->unrelate($document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CAdvowareAkten;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\ORM\Repository\Option\SaveOptions;
|
||||||
|
use Espo\Core\Hook\Hook\BeforeSave;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Prüft Junction-Table und aktualisiert globalen syncStatus
|
||||||
|
* basierend auf den syncstatus-Werten der verknüpften Dokumente
|
||||||
|
*/
|
||||||
|
class CheckGlobalSyncStatus implements BeforeSave
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function beforeSave(Entity $entity, SaveOptions $options): void
|
||||||
|
{
|
||||||
|
// Überspringe, wenn skipHooks gesetzt ist (verhindert Loops)
|
||||||
|
if ($options->get('skipHooks')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur wenn Entity bereits existiert (nicht bei Create)
|
||||||
|
if ($entity->isNew()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hole alle verknüpften Dokumente mit ihren syncstatus-Werten aus der Junction-Tabelle
|
||||||
|
$query = $this->entityManager->getQueryBuilder()
|
||||||
|
->select(['syncstatus'])
|
||||||
|
->from('CAdvowareAktenDokumente')
|
||||||
|
->where([
|
||||||
|
'cAdvowareAktenId' => $entity->getId(),
|
||||||
|
'deleted' => false
|
||||||
|
])
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$pdoStatement = $this->entityManager->getQueryExecutor()->execute($query);
|
||||||
|
$rows = $pdoStatement->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Wenn keine Dokumente verknüpft, setze auf "unclean"
|
||||||
|
if (empty($rows)) {
|
||||||
|
$entity->set('syncStatus', 'unclean');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe, ob irgendein Dokument "new" oder "unclean" ist
|
||||||
|
$hasUnsynced = false;
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$status = $row['syncstatus'] ?? null;
|
||||||
|
if ($status === 'new' || $status === 'unclean' || $status === null || $status === '') {
|
||||||
|
$hasUnsynced = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setze globalen Status
|
||||||
|
if ($hasUnsynced) {
|
||||||
|
$entity->set('syncStatus', 'unclean');
|
||||||
|
} else {
|
||||||
|
// Alle Dokumente sind "synced"
|
||||||
|
$entity->set('syncStatus', 'synced');
|
||||||
|
$entity->set('lastSync', date('Y-m-d H:i:s'));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Bei Fehler loggen und Status auf "unclean" setzen
|
||||||
|
$GLOBALS['log']->error('CAdvowareAkten CheckGlobalSyncStatus Hook Error: ' . $e->getMessage());
|
||||||
|
$entity->set('syncStatus', 'unclean');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
custom/Espo/Custom/Hooks/CAdvowareAkten/PropagateDocumentsUp.php
Normal file
118
custom/Espo/Custom/Hooks/CAdvowareAkten/PropagateDocumentsUp.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CAdvowareAkten;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Core\Hook\Hook\AfterSave;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Propagiert Dokumenten-Änderungen von AdvowareAkten nach oben zu Räumungsklage/Mietinkasso
|
||||||
|
* und auch zu AICollection
|
||||||
|
*
|
||||||
|
* Wenn ein Dokument einer AdvowareAkte zugewiesen wird (via cAdvowareAktenId):
|
||||||
|
* → verknüpfe mit verbundener Räumungsklage/Mietinkasso
|
||||||
|
* → verknüpfe mit AICollection
|
||||||
|
*
|
||||||
|
* Improved logic: Works with direct belongsTo relationship (cAdvowareAktenId)
|
||||||
|
*/
|
||||||
|
class PropagateDocumentsUp implements AfterSave
|
||||||
|
{
|
||||||
|
private static array $processing = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterSave(Entity $entity, \Espo\ORM\Repository\Option\SaveOptions $options): void
|
||||||
|
{
|
||||||
|
// Only process when cAdvowareAktenId changed
|
||||||
|
if (!$entity->isAttributeChanged('cAdvowareAktenId')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$akteId = $entity->get('cAdvowareAktenId');
|
||||||
|
if (!$akteId) {
|
||||||
|
return; // Document was unlinked from Akte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $akteId . '-' . $entity->getId() . '-propagate';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load AdvowareAkte
|
||||||
|
$akte = $this->entityManager->getEntity('CAdvowareAkten', $akteId);
|
||||||
|
if (!$akte) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob Räumungsklage verknüpft ist
|
||||||
|
$raumungsklage = $this->entityManager
|
||||||
|
->getRDBRepository('CAdvowareAkten')
|
||||||
|
->getRelation($akte, 'vmhRumungsklage')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($raumungsklage) {
|
||||||
|
$this->relateDocument($raumungsklage, 'dokumentesvmhraumungsklage', $entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob Mietinkasso verknüpft ist
|
||||||
|
$mietinkasso = $this->entityManager
|
||||||
|
->getRDBRepository('CAdvowareAkten')
|
||||||
|
->getRelation($akte, 'mietinkasso')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($mietinkasso) {
|
||||||
|
$this->relateDocument($mietinkasso, 'dokumentesmietinkasso', $entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also propagate to AICollection if Räumungsklage or Mietinkasso has one
|
||||||
|
if ($raumungsklage) {
|
||||||
|
$aiKnowledge = $this->entityManager
|
||||||
|
->getRDBRepository('CVmhRumungsklage')
|
||||||
|
->getRelation($raumungsklage, 'aIKnowledge')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($aiKnowledge) {
|
||||||
|
$this->relateDocument($aiKnowledge, 'dokumentes', $entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mietinkasso) {
|
||||||
|
$aiKnowledge = $this->entityManager
|
||||||
|
->getRDBRepository('CMietinkasso')
|
||||||
|
->getRelation($mietinkasso, 'aIKnowledge')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($aiKnowledge) {
|
||||||
|
$this->relateDocument($aiKnowledge, 'dokumentes', $entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CAdvowareAkten PropagateDocumentsUp Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion: Verknüpfe Dokument (nur wenn nicht bereits verknüpft)
|
||||||
|
*/
|
||||||
|
private function relateDocument(Entity $parentEntity, string $relationName, Entity $document): void
|
||||||
|
{
|
||||||
|
$repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType());
|
||||||
|
$relation = $repository->getRelation($parentEntity, $relationName);
|
||||||
|
|
||||||
|
// Prüfe ob bereits verknüpft
|
||||||
|
$isRelated = $relation
|
||||||
|
->where(['id' => $document->getId()])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if (!$isRelated) {
|
||||||
|
$relation->relate($document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,22 +35,24 @@ class CDokumente extends \Espo\Core\Hooks\Base
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Berechne neue Hashes
|
// Berechne Blake3 Hash
|
||||||
$newMd5 = hash_file('md5', $filePath);
|
$fileContent = file_get_contents($filePath);
|
||||||
$newSha256 = hash_file('sha256', $filePath);
|
if ($fileContent === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Setze Hashes
|
$newBlake3 = \blake3($fileContent);
|
||||||
$entity->set('md5sum', $newMd5);
|
|
||||||
$entity->set('sha256', $newSha256);
|
// Setze Hash
|
||||||
|
$entity->set('blake3hash', $newBlake3);
|
||||||
|
|
||||||
// Bestimme Status
|
// Bestimme Status
|
||||||
if ($entity->isNew()) {
|
if ($entity->isNew()) {
|
||||||
$entity->set('fileStatus', 'new');
|
$entity->set('fileStatus', 'new');
|
||||||
} else {
|
} else {
|
||||||
$oldMd5 = $entity->getFetched('md5sum');
|
$oldBlake3 = $entity->getFetched('blake3hash');
|
||||||
$oldSha256 = $entity->getFetched('sha256');
|
|
||||||
|
|
||||||
if ($oldMd5 !== $newMd5 || $oldSha256 !== $newSha256) {
|
if ($oldBlake3 !== $newBlake3) {
|
||||||
$entity->set('fileStatus', 'changed');
|
$entity->set('fileStatus', 'changed');
|
||||||
} else {
|
} else {
|
||||||
$entity->set('fileStatus', 'synced');
|
$entity->set('fileStatus', 'synced');
|
||||||
|
|||||||
122
custom/Espo/Custom/Hooks/CDokumente/UpdateJunctionSyncStatus.php
Normal file
122
custom/Espo/Custom/Hooks/CDokumente/UpdateJunctionSyncStatus.php
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CDokumente;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\ORM\Repository\Option\SaveOptions;
|
||||||
|
use Espo\Core\Hook\Hook\AfterSave;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Bei Änderung eines Dokuments wird syncStatus auf "unclean" gesetzt
|
||||||
|
* und alle verknüpften AIKnowledge Junction-Table-Einträge werden aktualisiert
|
||||||
|
*/
|
||||||
|
class UpdateJunctionSyncStatus implements AfterSave
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterSave(Entity $entity, SaveOptions $options): void
|
||||||
|
{
|
||||||
|
// Überspringe bei Create (nur bei Update)
|
||||||
|
if ($entity->isNew()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Überspringe, wenn keine relevanten Felder geändert wurden
|
||||||
|
if (!$this->hasRelevantChanges($entity)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Set syncStatus = 'unclean' directly on CDokumente entity
|
||||||
|
// (only if it has an AdvowareAkte linked)
|
||||||
|
if ($entity->get('cAdvowareAktenId')) {
|
||||||
|
$entity->set('syncStatus', 'unclean');
|
||||||
|
$this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
|
||||||
|
// Also update the parent AdvowareAkte
|
||||||
|
$akte = $this->entityManager->getEntity('CAdvowareAkten', $entity->get('cAdvowareAktenId'));
|
||||||
|
if ($akte) {
|
||||||
|
$akte->set('syncStatus', 'unclean');
|
||||||
|
$this->entityManager->saveEntity($akte, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update AIKnowledge Junction-Tables (unchanged)
|
||||||
|
$this->updateAIKnowledgeJunctions($entity);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Fehler loggen, aber nicht werfen (um Save nicht zu blockieren)
|
||||||
|
$GLOBALS['log']->error('CDokumente UpdateJunctionSyncStatus Hook Error: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob relevante Felder geändert wurden
|
||||||
|
*/
|
||||||
|
private function hasRelevantChanges(Entity $entity): bool
|
||||||
|
{
|
||||||
|
// Relevante Felder für Sync-Status
|
||||||
|
$relevantFields = [
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'dokument',
|
||||||
|
'dokumentId',
|
||||||
|
'preview',
|
||||||
|
'previewId',
|
||||||
|
'fileStatus'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($relevantFields as $field) {
|
||||||
|
if ($entity->isAttributeChanged($field)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update AIKnowledge Junction-Tables
|
||||||
|
*/
|
||||||
|
private function updateAIKnowledgeJunctions(Entity $entity): void
|
||||||
|
{
|
||||||
|
$updateQuery = $this->entityManager->getQueryBuilder()
|
||||||
|
->update()
|
||||||
|
->in('CAIKnowledgeDokumente')
|
||||||
|
->set(['syncstatus' => 'unclean'])
|
||||||
|
->where([
|
||||||
|
'cDokumenteId' => $entity->getId(),
|
||||||
|
'deleted' => false
|
||||||
|
])
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$this->entityManager->getQueryExecutor()->execute($updateQuery);
|
||||||
|
|
||||||
|
// Hole alle betroffenen AIKnowledge IDs
|
||||||
|
$selectQuery = $this->entityManager->getQueryBuilder()
|
||||||
|
->select(['cAIKnowledgeId'])
|
||||||
|
->from('CAIKnowledgeDokumente')
|
||||||
|
->where([
|
||||||
|
'cDokumenteId' => $entity->getId(),
|
||||||
|
'deleted' => false
|
||||||
|
])
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$pdoStatement = $this->entityManager->getQueryExecutor()->execute($selectQuery);
|
||||||
|
$rows = $pdoStatement->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Trigger Update auf jeder AIKnowledge (um CheckGlobalSyncStatus Hook auszulösen)
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$knowledgeId = $row['cAIKnowledgeId'] ?? null;
|
||||||
|
if ($knowledgeId) {
|
||||||
|
$knowledge = $this->entityManager->getEntity('CAIKnowledge', $knowledgeId);
|
||||||
|
if ($knowledge) {
|
||||||
|
// Force Update ohne Hook-Loop
|
||||||
|
$knowledge->set('syncStatus', 'unclean');
|
||||||
|
$this->entityManager->saveEntity($knowledge, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
custom/Espo/Custom/Hooks/CKuendigung/CreateAdvowareAkte.php
Normal file
134
custom/Espo/Custom/Hooks/CKuendigung/CreateAdvowareAkte.php
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CKuendigung;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Core\Hook\Hook\AfterSave;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Erstellt automatisch AdvowareAkte für Kündigung
|
||||||
|
*
|
||||||
|
* Wenn eine Kündigung erstellt/gespeichert wird:
|
||||||
|
* - Prüfe ob bereits eine AdvowareAkte vorhanden ist (über verknüpfte Räumungsklage)
|
||||||
|
* - Wenn nein: Erstelle neue AdvowareAkte und verknüpfe sie
|
||||||
|
*/
|
||||||
|
class CreateAdvowareAkte implements AfterSave
|
||||||
|
{
|
||||||
|
private static array $processing = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager,
|
||||||
|
private \Espo\Core\InjectableFactory $injectableFactory
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterSave(
|
||||||
|
Entity $entity,
|
||||||
|
\Espo\ORM\Repository\Option\SaveOptions $options
|
||||||
|
): void {
|
||||||
|
// Skip if silent or during hooks
|
||||||
|
if ($options->get('silent') || $options->get('skipHooks')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-create-akte';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prüfe ob Kündigung bereits eine AdvowareAkte hat
|
||||||
|
$existingAkteId = $entity->get('advowareAktenId');
|
||||||
|
|
||||||
|
if ($existingAkteId) {
|
||||||
|
$GLOBALS['log']->info("CKuendigung CreateAdvowareAkte: Kündigung already has AdvowareAkte: {$existingAkteId}");
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
return; // Bereits vorhanden
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob verknüpfte Räumungsklagen eine Akte haben
|
||||||
|
$raeumungsklagen = $this->entityManager
|
||||||
|
->getRDBRepository('CKuendigung')
|
||||||
|
->getRelation($entity, 'vmhRumungsklages')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
foreach ($raeumungsklagen as $rk) {
|
||||||
|
$rkAkteId = $rk->get('advowareAktenId');
|
||||||
|
if ($rkAkteId) {
|
||||||
|
// Übernehme Akte von Räumungsklage
|
||||||
|
$entity->set('advowareAktenId', $rkAkteId);
|
||||||
|
$this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
|
||||||
|
// Synchronisiere Aktennummer
|
||||||
|
$akte = $this->entityManager->getEntity('CAdvowareAkten', $rkAkteId);
|
||||||
|
if ($akte) {
|
||||||
|
$this->syncAktennummer($entity, $akte);
|
||||||
|
}
|
||||||
|
|
||||||
|
$GLOBALS['log']->info("CKuendigung CreateAdvowareAkte: Using AdvowareAkte from Räumungsklage: {$rkAkteId}");
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keine Akte gefunden -> Erstelle neue
|
||||||
|
$this->createNewAkte($entity);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CKuendigung CreateAdvowareAkte Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createNewAkte(Entity $kuendigung): void
|
||||||
|
{
|
||||||
|
// Hole Aktennummer aus Kündigung (falls vorhanden)
|
||||||
|
$aktennummer = $kuendigung->get('aktennr');
|
||||||
|
if (!$aktennummer) {
|
||||||
|
$aktennummer = time();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erstelle AdvowareAkte (aktenzeichen bleibt leer)
|
||||||
|
$akteData = [
|
||||||
|
'name' => 'Advoware Akte - ' . $kuendigung->get('name'),
|
||||||
|
'aktennummer' => $aktennummer,
|
||||||
|
'syncStatus' => 'unclean',
|
||||||
|
'assignedUserId' => $kuendigung->get('assignedUserId')
|
||||||
|
];
|
||||||
|
|
||||||
|
// Copy teams
|
||||||
|
$teamsIds = $kuendigung->getLinkMultipleIdList('teams');
|
||||||
|
if (!empty($teamsIds)) {
|
||||||
|
$akteData['teamsIds'] = $teamsIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
$akte = $this->entityManager->createEntity('CAdvowareAkten', $akteData);
|
||||||
|
|
||||||
|
if ($akte) {
|
||||||
|
// Verknüpfe mit Kündigung
|
||||||
|
$kuendigung->set('advowareAktenId', $akte->getId());
|
||||||
|
$this->entityManager->saveEntity($kuendigung, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
|
||||||
|
// Synchronisiere Aktennummer zurück zur Kündigung (falls leer war)
|
||||||
|
if (!$kuendigung->get('aktennr') && $akte->get('aktennummer')) {
|
||||||
|
$kuendigung->set('aktennr', $akte->get('aktennummer'));
|
||||||
|
$this->entityManager->saveEntity($kuendigung, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$GLOBALS['log']->info("CKuendigung CreateAdvowareAkte: Created new AdvowareAkte: {$akte->getId()}");
|
||||||
|
} else {
|
||||||
|
$GLOBALS['log']->error('CKuendigung CreateAdvowareAkte: Failed to create AdvowareAkte');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncAktennummer(Entity $kuendigung, Entity $akte): void
|
||||||
|
{
|
||||||
|
// Synchronisiere nur Aktennummer (nicht Aktenzeichen, das ist in der Beziehung)
|
||||||
|
if (!$kuendigung->get('aktennr') && $akte->get('aktennummer')) {
|
||||||
|
$kuendigung->set('aktennr', $akte->get('aktennummer'));
|
||||||
|
$this->entityManager->saveEntity($kuendigung, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
$GLOBALS['log']->info("CKuendigung CreateAdvowareAkte: Synchronized Aktennummer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
custom/Espo/Custom/Hooks/CKuendigung/SyncAdvowareAkte.php
Normal file
84
custom/Espo/Custom/Hooks/CKuendigung/SyncAdvowareAkte.php
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CKuendigung;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Core\Hook\Hook\AfterRelate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Synchronisiert AdvowareAkte zwischen Kündigung und Räumungsklage
|
||||||
|
*
|
||||||
|
* Wenn eine Kündigung mit einer Räumungsklage verknüpft wird:
|
||||||
|
* - Prüfe ob Räumungsklage eine AdvowareAkte hat
|
||||||
|
* - Wenn ja, verknüpfe diese Akte auch mit der Kündigung
|
||||||
|
* - Übernehme/Synchronisiere Aktennummer und Aktenzeichen
|
||||||
|
*/
|
||||||
|
class SyncAdvowareAkte implements AfterRelate
|
||||||
|
{
|
||||||
|
private static array $processing = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterRelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
array $columnData,
|
||||||
|
\Espo\ORM\Repository\Option\RelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für vmhRumungsklages-Beziehung (wenn Räumungsklage zu Kündigung hinzugefügt wird)
|
||||||
|
if ($relationName !== 'vmhRumungsklages') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-' . $foreignEntity->getId() . '-sync-akte';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// $entity = CKuendigung
|
||||||
|
// $foreignEntity = CVmhRumungsklage
|
||||||
|
|
||||||
|
// Hole AdvowareAkte von der Räumungsklage (hasOne relationship - get via field)
|
||||||
|
$advowareAkteId = $foreignEntity->get('advowareAktenId');
|
||||||
|
|
||||||
|
if ($advowareAkteId) {
|
||||||
|
$advowareAkte = $this->entityManager->getEntity('CAdvowareAkten', $advowareAkteId);
|
||||||
|
|
||||||
|
if ($advowareAkte) {
|
||||||
|
$GLOBALS['log']->info("CKuendigung SyncAdvowareAkte: Found AdvowareAkte {$advowareAkte->getId()} on Räumungsklage {$foreignEntity->getId()}");
|
||||||
|
|
||||||
|
// Prüfe ob Kündigung bereits eine andere Akte hat
|
||||||
|
$existingAktenId = $entity->get('advowareAktenId');
|
||||||
|
|
||||||
|
if ($existingAktenId && $existingAktenId !== $advowareAkteId) {
|
||||||
|
$GLOBALS['log']->warning("CKuendigung SyncAdvowareAkte: Kündigung already has different AdvowareAkte {$existingAktenId}, will replace with {$advowareAkteId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verknüpfe AdvowareAkte mit Kündigung (belongsTo relationship - set field directly)
|
||||||
|
$entity->set('advowareAktenId', $advowareAkteId);
|
||||||
|
|
||||||
|
// Synchronisiere nur Aktennummer (Aktenzeichen kommt über Beziehung)
|
||||||
|
if (!$entity->get('aktennr') && $advowareAkte->get('aktennummer')) {
|
||||||
|
$entity->set('aktennr', $advowareAkte->get('aktennummer'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save once with all changes
|
||||||
|
$this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
$GLOBALS['log']->info("CKuendigung SyncAdvowareAkte: Successfully linked AdvowareAkte and synchronized Aktennummer to Kündigung");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$GLOBALS['log']->info("CKuendigung SyncAdvowareAkte: Räumungsklage {$foreignEntity->getId()} has no AdvowareAkte yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CKuendigung SyncAdvowareAkte Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
custom/Espo/Custom/Hooks/CMietinkasso/PropagateDocuments.php
Normal file
157
custom/Espo/Custom/Hooks/CMietinkasso/PropagateDocuments.php
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CMietinkasso;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Core\Hook\Hook\AfterRelate;
|
||||||
|
use Espo\Core\Hook\Hook\AfterUnrelate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Propagiert Dokumenten-Verknüpfungen von Mietinkasso zu AdvowareAkten und AIKnowledge
|
||||||
|
*
|
||||||
|
* - Wenn Dokument mit Mietinkasso verknüpft wird → verknüpfe auch mit AdvowareAkten + AIKnowledge
|
||||||
|
* - Wenn Dokument von Mietinkasso entknüpft wird → entknüpfe auch von AdvowareAkten + AIKnowledge
|
||||||
|
*/
|
||||||
|
class PropagateDocuments implements AfterRelate, AfterUnrelate
|
||||||
|
{
|
||||||
|
private static array $processing = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterRelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
array $columnData,
|
||||||
|
\Espo\ORM\Repository\Option\RelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für dokumentesmietinkasso-Beziehung
|
||||||
|
if ($relationName !== 'dokumentesmietinkasso') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-' . $foreignEntity->getId() . '-relate';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hole verbundene AdvowareAkten
|
||||||
|
$advowareAkten = $this->entityManager
|
||||||
|
->getRDBRepository('CMietinkasso')
|
||||||
|
->getRelation($entity, 'advowareAkten')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Set direct belongsTo relationship on document
|
||||||
|
if ($advowareAkten) {
|
||||||
|
$foreignEntity->set('cAdvowareAktenId', $advowareAkten->getId());
|
||||||
|
$foreignEntity->set('syncStatus', 'new'); // Mark as new for Advoware sync
|
||||||
|
$this->entityManager->saveEntity($foreignEntity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hole verbundene AIKnowledge
|
||||||
|
$aIKnowledge = $this->entityManager
|
||||||
|
->getRDBRepository('CMietinkasso')
|
||||||
|
->getRelation($entity, 'aIKnowledge')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Verknüpfe Dokument mit AIKnowledge
|
||||||
|
if ($aIKnowledge) {
|
||||||
|
$this->relateDocument($aIKnowledge, 'dokumentes', $foreignEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CMietinkasso PropagateDocuments (relate) Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function afterUnrelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
\Espo\ORM\Repository\Option\UnrelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für dokumentesmietinkasso-Beziehung
|
||||||
|
if ($relationName !== 'dokumentesmietinkasso') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-' . $foreignEntity->getId() . '-unrelate';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hole verbundene AdvowareAkten
|
||||||
|
$advowareAkten = $this->entityManager
|
||||||
|
->getRDBRepository('CMietinkasso')
|
||||||
|
->getRelation($entity, 'advowareAkten')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Remove direct belongsTo relationship from document
|
||||||
|
if ($advowareAkten && $foreignEntity->get('cAdvowareAktenId') === $advowareAkten->getId()) {
|
||||||
|
$foreignEntity->set('cAdvowareAktenId', null);
|
||||||
|
$this->entityManager->saveEntity($foreignEntity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hole verbundene AIKnowledge
|
||||||
|
$aIKnowledge = $this->entityManager
|
||||||
|
->getRDBRepository('CMietinkasso')
|
||||||
|
->getRelation($entity, 'aIKnowledge')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Entknüpfe Dokument von AIKnowledge
|
||||||
|
if ($aIKnowledge) {
|
||||||
|
$this->unrelateDocument($aIKnowledge, 'dokumentes', $foreignEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CMietinkasso PropagateDocuments (unrelate) Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion: Verknüpfe Dokument (nur wenn nicht bereits verknüpft)
|
||||||
|
*/
|
||||||
|
private function relateDocument(Entity $parentEntity, string $relationName, Entity $document): void
|
||||||
|
{
|
||||||
|
$repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType());
|
||||||
|
$relation = $repository->getRelation($parentEntity, $relationName);
|
||||||
|
|
||||||
|
// Prüfe ob bereits verknüpft
|
||||||
|
$isRelated = $relation
|
||||||
|
->where(['id' => $document->getId()])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if (!$isRelated) {
|
||||||
|
$relation->relate($document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion: Entknüpfe Dokument
|
||||||
|
*/
|
||||||
|
private function unrelateDocument(Entity $parentEntity, string $relationName, Entity $document): void
|
||||||
|
{
|
||||||
|
$repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType());
|
||||||
|
$relation = $repository->getRelation($parentEntity, $relationName);
|
||||||
|
|
||||||
|
// Prüfe ob verknüpft
|
||||||
|
$isRelated = $relation
|
||||||
|
->where(['id' => $document->getId()])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($isRelated) {
|
||||||
|
$relation->unrelate($document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
custom/Espo/Custom/Hooks/CVmhRumungsklage/PropagateDocuments.php
Normal file
157
custom/Espo/Custom/Hooks/CVmhRumungsklage/PropagateDocuments.php
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CVmhRumungsklage;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Core\Hook\Hook\AfterRelate;
|
||||||
|
use Espo\Core\Hook\Hook\AfterUnrelate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Propagiert Dokumenten-Verknüpfungen von Räumungsklage zu AdvowareAkten und AIKnowledge
|
||||||
|
*
|
||||||
|
* - Wenn Dokument mit Räumungsklage verknüpft wird → verknüpfe auch mit AdvowareAkten + AIKnowledge
|
||||||
|
* - Wenn Dokument von Räumungsklage entknüpft wird → entknüpfe auch von AdvowareAkten + AIKnowledge
|
||||||
|
*/
|
||||||
|
class PropagateDocuments implements AfterRelate, AfterUnrelate
|
||||||
|
{
|
||||||
|
private static array $processing = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterRelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
array $columnData,
|
||||||
|
\Espo\ORM\Repository\Option\RelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für dokumentesvmhraumungsklage-Beziehung
|
||||||
|
if ($relationName !== 'dokumentesvmhraumungsklage') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-' . $foreignEntity->getId() . '-relate';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hole verbundene AdvowareAkten
|
||||||
|
$advowareAkten = $this->entityManager
|
||||||
|
->getRDBRepository('CVmhRumungsklage')
|
||||||
|
->getRelation($entity, 'advowareAkten')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Set direct belongsTo relationship on document
|
||||||
|
if ($advowareAkten) {
|
||||||
|
$foreignEntity->set('cAdvowareAktenId', $advowareAkten->getId());
|
||||||
|
$foreignEntity->set('syncStatus', 'new'); // Mark as new for Advoware sync
|
||||||
|
$this->entityManager->saveEntity($foreignEntity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hole verbundene AIKnowledge
|
||||||
|
$aIKnowledge = $this->entityManager
|
||||||
|
->getRDBRepository('CVmhRumungsklage')
|
||||||
|
->getRelation($entity, 'aIKnowledge')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Verknüpfe Dokument mit AIKnowledge
|
||||||
|
if ($aIKnowledge) {
|
||||||
|
$this->relateDocument($aIKnowledge, 'dokumentes', $foreignEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CVmhRumungsklage PropagateDocuments (relate) Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function afterUnrelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
\Espo\ORM\Repository\Option\UnrelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für dokumentesvmhraumungsklage-Beziehung
|
||||||
|
if ($relationName !== 'dokumentesvmhraumungsklage') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-' . $foreignEntity->getId() . '-unrelate';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hole verbundene AdvowareAkten
|
||||||
|
$advowareAkten = $this->entityManager
|
||||||
|
->getRDBRepository('CVmhRumungsklage')
|
||||||
|
->getRelation($entity, 'advowareAkten')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Remove direct belongsTo relationship from document
|
||||||
|
if ($advowareAkten && $foreignEntity->get('cAdvowareAktenId') === $advowareAkten->getId()) {
|
||||||
|
$foreignEntity->set('cAdvowareAktenId', null);
|
||||||
|
$this->entityManager->saveEntity($foreignEntity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hole verbundene AIKnowledge
|
||||||
|
$aIKnowledge = $this->entityManager
|
||||||
|
->getRDBRepository('CVmhRumungsklage')
|
||||||
|
->getRelation($entity, 'aIKnowledge')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Entknüpfe Dokument von AIKnowledge
|
||||||
|
if ($aIKnowledge) {
|
||||||
|
$this->unrelateDocument($aIKnowledge, 'dokumentes', $foreignEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CVmhRumungsklage PropagateDocuments (unrelate) Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion: Verknüpfe Dokument (nur wenn nicht bereits verknüpft)
|
||||||
|
*/
|
||||||
|
private function relateDocument(Entity $parentEntity, string $relationName, Entity $document): void
|
||||||
|
{
|
||||||
|
$repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType());
|
||||||
|
$relation = $repository->getRelation($parentEntity, $relationName);
|
||||||
|
|
||||||
|
// Prüfe ob bereits verknüpft
|
||||||
|
$isRelated = $relation
|
||||||
|
->where(['id' => $document->getId()])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if (!$isRelated) {
|
||||||
|
$relation->relate($document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion: Entknüpfe Dokument
|
||||||
|
*/
|
||||||
|
private function unrelateDocument(Entity $parentEntity, string $relationName, Entity $document): void
|
||||||
|
{
|
||||||
|
$repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType());
|
||||||
|
$relation = $repository->getRelation($parentEntity, $relationName);
|
||||||
|
|
||||||
|
// Prüfe ob verknüpft
|
||||||
|
$isRelated = $relation
|
||||||
|
->where(['id' => $document->getId()])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($isRelated) {
|
||||||
|
$relation->unrelate($document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"links": {
|
|
||||||
"meetings": "الاجتماعات",
|
|
||||||
"calls": "المكالمات",
|
|
||||||
"tasks": "مهام"
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"Create CAICollection": "إنشاء {الكيانTypeTranslated}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledge": "إنشاء {الكيانTypeTranslated}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"links": {
|
|
||||||
"meetings": "Срещи",
|
|
||||||
"calls": "Разговори",
|
|
||||||
"tasks": "Задачи"
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"Create CAICollection": "Създаване на AI Collection"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledge": "Създаване на AI Knowledge"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,6 @@
|
|||||||
"tasks": "Задачи"
|
"tasks": "Задачи"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CAdvowareAkten": "Създаване на Advoware Akte"
|
"Create CAdvowareAkten": "Създаване на Advoware Akten"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"links": {
|
|
||||||
"meetings": "Schůzky",
|
|
||||||
"calls": "Hovory",
|
|
||||||
"tasks": "Úkoly"
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"Create CAICollection": "Vytvořit AI Collection"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledge": "Vytvořit AI Knowledge"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,6 @@
|
|||||||
"tasks": "Úkoly"
|
"tasks": "Úkoly"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CAdvowareAkten": "Vytvořit Advoware Akte"
|
"Create CAdvowareAkten": "Vytvořit Advoware Akten"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"links": {
|
|
||||||
"meetings": "Møder",
|
|
||||||
"calls": "Opkald",
|
|
||||||
"tasks": "Opgaver"
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"Create CAICollection": "Opret AI Collection "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledge": "Opret AI Knowledge "
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,6 @@
|
|||||||
"tasks": "Opgaver"
|
"tasks": "Opgaver"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CAdvowareAkten": "Opret Advoware Akte "
|
"Create CAdvowareAkten": "Opret Advoware Akten "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"fields": {
|
|
||||||
"cAdvowareAkte": "Advoware-Akte",
|
|
||||||
"cAdvowareAkteId": "Advoware-Akte ID",
|
|
||||||
"cAdvowareAkteName": "Advoware-Akte Name",
|
|
||||||
"xaiCollectionId": "x.AI Collection ID",
|
|
||||||
"syncStatus": "Sync-Status",
|
|
||||||
"lastSync": "Letzte Synchronisation",
|
|
||||||
"cmietinkasso": "Mietinkasso",
|
|
||||||
"cmietinkassoId": "Mietinkasso ID",
|
|
||||||
"cmietinkassoName": "Mietinkasso Name",
|
|
||||||
"cvmhRumungsklage": "Räumungsklage",
|
|
||||||
"cvmhRumungsklageId": "Räumungsklage ID",
|
|
||||||
"cvmhRumungsklageName": "Räumungsklage Name"
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"meetings": "Termine",
|
|
||||||
"calls": "Anrufe",
|
|
||||||
"tasks": "Aufgaben",
|
|
||||||
"cDokumente": "Dokumente",
|
|
||||||
"cAdvowareAkte": "Advoware-Akte",
|
|
||||||
"cmietinkasso": "Mietinkasso",
|
|
||||||
"cvmhRumungsklage": "Räumungsklage"
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"Create CAICollection": "AI Collection erstellen"
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"xaiCollectionId": "Collection ID für x.AI Synchronisation",
|
|
||||||
"syncStatus": "Status der x.AI-Synchronisation: pending_sync = Warte auf Sync, clean = erfolgreich synchronisiert, unclean = Änderungen ausstehend, failed = Fehler, no_sync = Nicht synchronisiert",
|
|
||||||
"lastSync": "Zeitpunkt der letzten Synchronisation mit x.AI"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"syncStatus": {
|
|
||||||
"pending_sync": "Warte auf Sync",
|
|
||||||
"clean": "Synchronisiert",
|
|
||||||
"unclean": "Änderungen ausstehend",
|
|
||||||
"failed": "Fehlgeschlagen",
|
|
||||||
"no_sync": "Kein Sync"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"labels": {
|
|
||||||
"Create CAICollectionCDokumente": "AI-Collection-Dokument-Verknüpfung erstellen",
|
|
||||||
"CAICollectionCDokumente": "AI-Collection-Dokument-Verknüpfungen"
|
|
||||||
},
|
|
||||||
"fields": {
|
|
||||||
"cAICollection": "AI-Collection",
|
|
||||||
"cAICollectionId": "AI-Collection ID",
|
|
||||||
"cDokumente": "Dokument",
|
|
||||||
"cDokumenteId": "Dokument ID",
|
|
||||||
"xaifileid": "XAI File ID",
|
|
||||||
"syncStatus": "Sync-Status",
|
|
||||||
"deleted": "Gelöscht"
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"cAICollection": "AI-Collection",
|
|
||||||
"cDokumente": "Dokument"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"syncStatus": {
|
|
||||||
"new": "Neu",
|
|
||||||
"changed": "Geändert",
|
|
||||||
"synced": "Synchronisiert",
|
|
||||||
"deleted": "Gelöscht"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"xaifileid": "Externe XAI File ID für dieses Dokument",
|
|
||||||
"syncStatus": "Synchronisierungsstatus mit XAI"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
49
custom/Espo/Custom/Resources/i18n/de_DE/CAIKnowledge.json
Normal file
49
custom/Espo/Custom/Resources/i18n/de_DE/CAIKnowledge.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledge": "AI Knowledge erstellen"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"dokumentes": "Dokumente",
|
||||||
|
"vmhRumungsklage": "Räumungsklage",
|
||||||
|
"mietinkasso": "Mietinkasso",
|
||||||
|
"datenbankId": "Datenbank-ID",
|
||||||
|
"syncStatus": "Sync-Status",
|
||||||
|
"lastSync": "Letzte Synchronisation",
|
||||||
|
"aktivierungsstatus": "Aktivierungsstatus",
|
||||||
|
"dokumenteAiDocumentId": "AI Document ID",
|
||||||
|
"dokumenteSyncstatus": "Sync-Status",
|
||||||
|
"dokumenteLastSync": "Letzter Sync",
|
||||||
|
"dokumenteSyncedHash": "Sync-Hash"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"dokumentes": "Dokumente",
|
||||||
|
"vmhRumungsklage": "Räumungsklage",
|
||||||
|
"mietinkasso": "Mietinkasso"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"syncStatus": {
|
||||||
|
"synced": "Synchronisiert",
|
||||||
|
"unclean": "Nicht synchronisiert",
|
||||||
|
"pending_sync": "Synchronisierung ausstehend"
|
||||||
|
},
|
||||||
|
"aktivierungsstatus": {
|
||||||
|
"new": "Neu",
|
||||||
|
"active": "Aktiv",
|
||||||
|
"paused": "Pausiert",
|
||||||
|
"deactivated": "Deaktiviert"
|
||||||
|
},
|
||||||
|
"dokumenteSyncstatus": {
|
||||||
|
"new": "Neu",
|
||||||
|
"unclean": "Nicht synchronisiert",
|
||||||
|
"synced": "Synchronisiert",
|
||||||
|
"failed": "Fehlgeschlagen",
|
||||||
|
"unsupported": "Nicht unterstützt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"syncStatus": "Globaler Synchronisationsstatus: synced = Alle Dokumente synchronisiert, unclean = Mindestens ein Dokument ist neu oder hat Änderungen, pending_sync = Synchronisierung wurde gestartet aber noch nicht abgeschlossen. Wird automatisch basierend auf den Dokumenten-Status aktualisiert.",
|
||||||
|
"lastSync": "Zeitpunkt der letzten erfolgreichen Synchronisation aller Dokumente",
|
||||||
|
"aktivierungsstatus": "Aktivierungsstatus des AI Knowledge Entries: new = Neu angelegt, active = Aktiv synchronisiert, paused = Synchronisation pausiert, deactivated = Synchronisation deaktiviert",
|
||||||
|
"datenbankId": "Eindeutige ID in der AI-Datenbank"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"cAIKnowledge": "AI Knowledge",
|
||||||
|
"cDokumente": "Dokument",
|
||||||
|
"aiDocumentId": "AI Dokument-ID",
|
||||||
|
"syncstatus": "Sync-Status",
|
||||||
|
"lastSync": "Letzte Synchronisation",
|
||||||
|
"syncedHash": "Sync-Hash"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"cAIKnowledge": "AI Knowledge",
|
||||||
|
"cDokumente": "Dokument"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledgeCDokumente": "Verknüpfung erstellen"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"syncstatus": {
|
||||||
|
"new": "Neu",
|
||||||
|
"unclean": "Geändert",
|
||||||
|
"synced": "Synchronisiert",
|
||||||
|
"failed": "Fehler",
|
||||||
|
"unsupported": "Nicht unterstützt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"aiDocumentId": "Externe AI-Dokument-Referenz-ID",
|
||||||
|
"syncedHash": "Hash-Wert des zuletzt synchronisierten Dokument-Zustands"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"cAIKnowledge": "AI Knowledge",
|
||||||
|
"cDokumente": "Dokument",
|
||||||
|
"aiDocumentId": "AI Dokument-ID",
|
||||||
|
"syncstatus": "Sync-Status",
|
||||||
|
"lastSync": "Letzte Synchronisation",
|
||||||
|
"syncedHash": "Sync-Hash"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"cAIKnowledge": "AI Knowledge",
|
||||||
|
"cDokumente": "Dokument"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledgeCDokumente": "Verknüpfung erstellen"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"syncstatus": {
|
||||||
|
"new": "Neu",
|
||||||
|
"unclean": "Geändert",
|
||||||
|
"synced": "Synchronisiert",
|
||||||
|
"failed": "Fehler",
|
||||||
|
"unsupported": "Nicht unterstützt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"aiDocumentId": "Externe AI-Dokument-Referenz-ID",
|
||||||
|
"syncedHash": "Hash-Wert des zuletzt synchronisierten Dokument-Zustands"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +1,49 @@
|
|||||||
{
|
{
|
||||||
"fields": {
|
"links": {
|
||||||
"advowareAktenzeichen": "Advoware Aktenzeichen",
|
"calls": "Anrufe",
|
||||||
"aktennr": "Advoware Identifikator",
|
"tasks": "Aufgaben",
|
||||||
"syncStatus": "Sync-Status",
|
"vmhRumungsklage": "Räumungsklagen",
|
||||||
"lastSync": "Letzte Synchronisation",
|
"mietinkasso": "Mietinkasso",
|
||||||
"cmietinkasso": "Mietinkasso",
|
"kuendigungen": "Kündigungen",
|
||||||
"cmietinkassoId": "Mietinkasso ID",
|
"dokumentes": "Dokumente"
|
||||||
"cmietinkassoName": "Mietinkasso Name",
|
},
|
||||||
"cvmhRumungsklage": "Räumungsklage",
|
"labels": {
|
||||||
"cvmhRumungsklageId": "Räumungsklage ID",
|
"Create CAdvowareAkten": "Advoware Akten erstellen"
|
||||||
"cvmhRumungsklageName": "Räumungsklage Name"
|
},
|
||||||
},
|
"fields": {
|
||||||
"links": {
|
"vmhRumungsklage": "Räumungsklagen",
|
||||||
"meetings": "Termine",
|
"mietinkasso": "Mietinkasso",
|
||||||
"calls": "Anrufe",
|
"aktenzeichen": "Aktenzeichen",
|
||||||
"tasks": "Aufgaben",
|
"aktennummer": "Aktennummer",
|
||||||
"cDokumente": "Dokumente",
|
"aktenpfad": "Aktenpfad (Windows)",
|
||||||
"cAICollection": "AI Collection",
|
"syncStatus": "Sync-Status",
|
||||||
"cmietinkasso": "Mietinkasso",
|
"lastSync": "Letzte Synchronisation",
|
||||||
"cvmhRumungsklage": "Räumungsklage"
|
"aktivierungsstatus": "Aktivierungsstatus",
|
||||||
},
|
"dokumentes": "Dokumente",
|
||||||
"labels": {
|
"dokumenteHnr": "HNR",
|
||||||
"Create CAdvowareAkten": "Advoware Akte erstellen"
|
"dokumenteSyncstatus": "Sync-Status",
|
||||||
},
|
"dokumenteLastSync": "Letzter Sync",
|
||||||
"tooltips": {
|
"dokumenteSyncedHash": "Sync-Hash"
|
||||||
"advowareAktenzeichen": "Aktenzeichen aus dem Advoware-System",
|
},
|
||||||
"aktennr": "Eindeutige Aktennummer aus Advoware",
|
"options": {
|
||||||
"syncStatus": "Status der Advoware-Synchronisation: pending_sync = Warte auf Sync, clean = erfolgreich synchronisiert, unclean = Änderungen ausstehend, failed = Fehler, no_sync = Nicht synchronisiert",
|
"syncStatus": {
|
||||||
"lastSync": "Zeitpunkt der letzten Synchronisation mit Advoware"
|
"synced": "Synchronisiert",
|
||||||
},
|
"unclean": "Nicht synchronisiert",
|
||||||
"options": {
|
"pending_sync": "Synchronisierung ausstehend",
|
||||||
"syncStatus": {
|
"failed": "Fehlgeschlagen"
|
||||||
"pending_sync": "Warte auf Sync",
|
},
|
||||||
"clean": "Synchronisiert",
|
"aktivierungsstatus": {
|
||||||
"unclean": "Änderungen ausstehend",
|
"new": "Neu",
|
||||||
"failed": "Fehlgeschlagen",
|
"import": "Import",
|
||||||
"no_sync": "Kein Sync"
|
"active": "Aktiv",
|
||||||
|
"paused": "Pausiert",
|
||||||
|
"deactivated": "Deaktiviert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"syncStatus": "Globaler Synchronisationsstatus: synced = Alle Dokumente synchronisiert, unclean = Mindestens ein Dokument ist neu oder hat Änderungen, pending_sync = Synchronisierung wurde gestartet aber noch nicht abgeschlossen, failed = Synchronisierung fehlgeschlagen. Wird automatisch basierend auf den Dokumenten-Status aktualisiert.",
|
||||||
|
"lastSync": "Zeitpunkt der letzten erfolgreichen Synchronisation aller Dokumente",
|
||||||
|
"aktivierungsstatus": "Aktivierungsstatus der Akte: new = Neu angelegt, import = Aus Advoware importiert, active = Aktiv synchronisiert, paused = Synchronisation pausiert, deactivated = Synchronisation deaktiviert",
|
||||||
|
"aktenpfad": "Windows-Dateipfad zur Akte in Advoware"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"cDokumenteId": "Dokument ID",
|
"cDokumenteId": "Dokument ID",
|
||||||
"hnr": "HNR",
|
"hnr": "HNR",
|
||||||
"syncStatus": "Sync-Status",
|
"syncStatus": "Sync-Status",
|
||||||
|
"syncedHash": "Sync-Hash",
|
||||||
"deleted": "Gelöscht"
|
"deleted": "Gelöscht"
|
||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"hnr": "Advoware HNR Referenz für dieses Dokument",
|
"hnr": "Advoware HNR Referenz für dieses Dokument",
|
||||||
"syncStatus": "Synchronisierungsstatus mit Advoware"
|
"syncStatus": "Synchronisierungsstatus mit Advoware",
|
||||||
|
"syncedHash": "Hash-Wert des zuletzt synchronisierten Dokument-Zustands (zur Änderungserkennung)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,27 +2,15 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"dokument": "Download",
|
"dokument": "Download",
|
||||||
"preview": "Vorschau",
|
"preview": "Vorschau",
|
||||||
"ydocumentuuid": "Y-Document-UUID",
|
"blake3hash": "Blake3-Hash",
|
||||||
"md5sum": "MD5-Prüfsumme",
|
"cAdvowareAkten": "Advoware Akte",
|
||||||
"sha256": "SHA256-Prüfsumme",
|
"cAdvowareAktenId": "Advoware Akten-ID",
|
||||||
"aktennr": "Advoware Identifikator",
|
"cAdvowareAktenName": "Advoware Aktenname",
|
||||||
"advowareLastSync": "Advoware letzte Synchronisation",
|
"hnr": "HNR (Advoware)",
|
||||||
"syncStatus": "Sync-Status",
|
"syncStatus": "Sync-Status",
|
||||||
"xaiId": "x.AI ID",
|
"syncedHash": "Sync-Hash",
|
||||||
"xaiCollections": "x.AI Collections",
|
"usn": "USN",
|
||||||
"xaiSyncStatus": "Sync-Status",
|
"dateipfad": "Dateipfad",
|
||||||
"fileStatus": "Datei-Status",
|
|
||||||
"contactsvmhdokumente": "Freigegebene Nutzer",
|
|
||||||
"vmhMietverhltnisesDokumente": "Mietverhältnisse",
|
|
||||||
"vmhErstgespraechsdokumente": "Erstgespräche",
|
|
||||||
"vmhRumungsklagesdokumente": "Räumungsklagen",
|
|
||||||
"kuendigungDokumente": "Kündigungen",
|
|
||||||
"beteiligte2dokumente": "Beteiligte",
|
|
||||||
"mietobjekt2dokumente": "Mietobjekte",
|
|
||||||
"mietinkassosdokumente": "Mietinkasso",
|
|
||||||
"kndigungensdokumente": "Kündigungen"
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"contactsvmhdokumente": "Freigegebene Nutzer",
|
"contactsvmhdokumente": "Freigegebene Nutzer",
|
||||||
"vmhMietverhltnisesDokumente": "Mietverhältnisse",
|
"vmhMietverhltnisesDokumente": "Mietverhältnisse",
|
||||||
"vmhErstgespraechsdokumente": "Erstgespräche",
|
"vmhErstgespraechsdokumente": "Erstgespräche",
|
||||||
@@ -32,40 +20,42 @@
|
|||||||
"mietobjekt2dokumente": "Mietobjekte",
|
"mietobjekt2dokumente": "Mietobjekte",
|
||||||
"mietinkassosdokumente": "Mietinkasso",
|
"mietinkassosdokumente": "Mietinkasso",
|
||||||
"kndigungensdokumente": "Kündigungen",
|
"kndigungensdokumente": "Kündigungen",
|
||||||
"cAICollections": "AI Collections",
|
"aIKnowledges": "AI Knowledge",
|
||||||
"cAdvowareAkten": "Advoware-Akten"
|
"aiKnowledgeAiDocumentId": "AI Document ID",
|
||||||
|
"aiKnowledgeSyncstatus": "AI Sync-Status",
|
||||||
|
"aiKnowledgeLastSync": "AI Letzter Sync"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"cAdvowareAkten": "Advoware Akte",
|
||||||
|
"contactsvmhdokumente": "Freigegebene Nutzer",
|
||||||
|
"vmhMietverhltnisesDokumente": "Mietverhältnisse",
|
||||||
|
"vmhErstgespraechsdokumente": "Erstgespräche",
|
||||||
|
"vmhRumungsklagesdokumente": "Räumungsklagen",
|
||||||
|
"kuendigungDokumente": "Kündigungen",
|
||||||
|
"beteiligte2dokumente": "Beteiligte",
|
||||||
|
"mietobjekt2dokumente": "Mietobjekte",
|
||||||
|
"mietinkassosdokumente": "Mietinkasso",
|
||||||
|
"kndigungensdokumente": "Kündigungen",
|
||||||
|
"aIKnowledges": "AI Knowledge"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CDokumente": "Dokument erstellen"
|
"Create CDokumente": "Dokument erstellen"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"aktennr": "Eindeutige Dokument-Nummer aus Advoware",
|
"blake3hash": "Kryptografischer Blake3-Hash der Datei (schneller und sicherer als MD5/SHA256)",
|
||||||
"advowareLastSync": "Zeitpunkt der letzten Synchronisation mit Advoware",
|
"hnr": "Hierarchische Referenznummer in Advoware",
|
||||||
"syncStatus": "Status der Advoware-Synchronisation: pending_sync = Warte auf Sync, clean = erfolgreich synchronisiert, unclean = Änderungen ausstehend, failed = Fehler, no_sync = Nicht synchronisiert",
|
"syncStatus": "Status der Synchronisation mit Advoware: new=neu, unclean=geändert, synced=synchronisiert, failed=Fehler, unsupported=nicht unterstützt",
|
||||||
"xaiId": "Eindeutige ID für x.AI Synchronisation",
|
"syncedHash": "Hash-Wert bei letzter erfolgreicher Synchronisation",
|
||||||
"xaiCollections": "Liste der x.AI Collections für dieses Dokument",
|
"usn": "Update Sequence Number - Versionsnummer für Synchronisation",
|
||||||
"xaiSyncStatus": "Status der x.AI Synchronisation: pending_sync = Warte auf Sync, clean = erfolgreich synchronisiert, unclean = Änderungen ausstehend, failed = Fehler, no_sync = Nicht synchronisiert",
|
"dateipfad": "Windows-Dateipfad des Dokuments in Advoware"
|
||||||
"fileStatus": "Status der Datei: new = neu hochgeladen, changed = geändert, synced = synchronisiert"
|
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"syncStatus": {
|
"syncStatus": {
|
||||||
"pending_sync": "Warte auf Sync",
|
|
||||||
"clean": "Synchronisiert",
|
|
||||||
"unclean": "Änderungen ausstehend",
|
|
||||||
"failed": "Fehlgeschlagen",
|
|
||||||
"no_sync": "Kein Sync"
|
|
||||||
},
|
|
||||||
"xaiSyncStatus": {
|
|
||||||
"pending_sync": "Warte auf Sync",
|
|
||||||
"clean": "Synchronisiert",
|
|
||||||
"unclean": "Abweichungen",
|
|
||||||
"failed": "Fehlgeschlagen",
|
|
||||||
"no_sync": "Kein Sync"
|
|
||||||
},
|
|
||||||
"fileStatus": {
|
|
||||||
"new": "Neu",
|
"new": "Neu",
|
||||||
"changed": "Geändert",
|
"unclean": "Geändert",
|
||||||
"synced": "Synchronisiert"
|
"synced": "Synchronisiert",
|
||||||
|
"failed": "Fehler",
|
||||||
|
"unsupported": "Nicht unterstützt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
"gekuendigte": "Mieter",
|
"gekuendigte": "Mieter",
|
||||||
"dokumenteskuendigung": "Dokumente",
|
"dokumenteskuendigung": "Dokumente",
|
||||||
"contactsKuendigung": "Portal-Freigaben",
|
"contactsKuendigung": "Portal-Freigaben",
|
||||||
|
"advowareAkten": "Advoware Akte",
|
||||||
|
"vmhRumungsklages": "Räumungsklagen",
|
||||||
"pulse": "Pulse"
|
"pulse": "Pulse"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
@@ -32,7 +34,7 @@
|
|||||||
"modifiedBy": "Geändert von",
|
"modifiedBy": "Geändert von",
|
||||||
"freigeschalteteNutzer": "Freigeschaltete Nutzer",
|
"freigeschalteteNutzer": "Freigeschaltete Nutzer",
|
||||||
"collaborators": "Mitarbeiter",
|
"collaborators": "Mitarbeiter",
|
||||||
"advowareAktenzeichen": "Advoware Aktenzeichen",
|
"advowareAkten": "Advoware Akte",
|
||||||
"aktennr": "Advoware Identifikator",
|
"aktennr": "Advoware Identifikator",
|
||||||
"advowareLastSync": "Letzter Sync",
|
"advowareLastSync": "Letzter Sync",
|
||||||
"syncStatus": "Sync Status",
|
"syncStatus": "Sync Status",
|
||||||
@@ -104,7 +106,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"advowareAktenzeichen": "Aktenzeichen aus Advoware für die Synchronisation",
|
|
||||||
"aktennr": "Eindeutige Kündigungs-Nummer aus Advoware",
|
"aktennr": "Eindeutige Kündigungs-Nummer aus Advoware",
|
||||||
"syncStatus": "Status der Advoware-Synchronisation: pending_sync = Warte auf Sync, clean = erfolgreich synchronisiert, unclean = Änderungen ausstehend, failed = Fehler, no_sync = Nicht synchronisiert",
|
"syncStatus": "Status der Advoware-Synchronisation: pending_sync = Warte auf Sync, clean = erfolgreich synchronisiert, unclean = Änderungen ausstehend, failed = Fehler, no_sync = Nicht synchronisiert",
|
||||||
"sendungsverfolgungsnummer": "Sendungsverfolgungsnummer für Einschreiben",
|
"sendungsverfolgungsnummer": "Sendungsverfolgungsnummer für Einschreiben",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"vmhMietverhltnises": "Mietverhältnisse",
|
"vmhMietverhltnises": "Mietverhältnisse",
|
||||||
"contactsMietinkasso": "Freigegebene Nutzer",
|
"contactsMietinkasso": "Freigegebene Nutzer",
|
||||||
"dokumentesmietinkasso": "Dokumente",
|
"dokumentesmietinkasso": "Dokumente",
|
||||||
"gerichtsrubrum": "Gerichtsrubrum",
|
|
||||||
"gegenstandswert": "Gegenstandswert",
|
"gegenstandswert": "Gegenstandswert",
|
||||||
"kuendigungsservice": "Kündigungsservice",
|
"kuendigungsservice": "Kündigungsservice",
|
||||||
"aussergerichtlicheGebuehren13": "Außergerichtliche Gebühren 1,3",
|
"aussergerichtlicheGebuehren13": "Außergerichtliche Gebühren 1,3",
|
||||||
@@ -14,8 +13,8 @@
|
|||||||
"freigeschalteteNutzer": "Freigeschaltete Nutzer (veraltet)",
|
"freigeschalteteNutzer": "Freigeschaltete Nutzer (veraltet)",
|
||||||
"collaborators": "Mitarbeiter",
|
"collaborators": "Mitarbeiter",
|
||||||
"vmhVermietersMIK": "Vermieter",
|
"vmhVermietersMIK": "Vermieter",
|
||||||
"cAdvowareAkte": "Advoware-Akte",
|
"advowareAkten": "Advoware Akten",
|
||||||
"cAICollection": "AI Collection"
|
"aIKnowledge": "AI Knowledge"
|
||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
"meetings": "Termine",
|
"meetings": "Termine",
|
||||||
@@ -30,14 +29,13 @@
|
|||||||
"collaborators": "Mitarbeiter",
|
"collaborators": "Mitarbeiter",
|
||||||
"vmhVermietersMIK": "Vermieter",
|
"vmhVermietersMIK": "Vermieter",
|
||||||
"pulse": "Pulse",
|
"pulse": "Pulse",
|
||||||
"cAdvowareAkte": "Advoware-Akte",
|
"advowareAkten": "Advoware Akten",
|
||||||
"cAICollection": "AI Collection"
|
"aIKnowledge": "AI Knowledge"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CMietinkasso": "Mietinkasso erstellen"
|
"Create CMietinkasso": "Mietinkasso erstellen"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"gerichtsrubrum": "Rubrum des Gerichtsverfahrens",
|
|
||||||
"gegenstandswert": "Wert des Streitgegenstands",
|
"gegenstandswert": "Wert des Streitgegenstands",
|
||||||
"kuendigungsservice": "Kündigungsservice aktiviert"
|
"kuendigungsservice": "Kündigungsservice aktiviert"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
"freigeschalteteNutzer": "Freigeschaltete Nutzer (veraltet)",
|
"freigeschalteteNutzer": "Freigeschaltete Nutzer (veraltet)",
|
||||||
"collaborators": "Mitarbeiter",
|
"collaborators": "Mitarbeiter",
|
||||||
"vmhVermietersRKL": "Vermieter",
|
"vmhVermietersRKL": "Vermieter",
|
||||||
"cAdvowareAkte": "Advoware-Akte",
|
"advowareAkten": "Advoware Akten",
|
||||||
"cAICollection": "AI Collection"
|
"aIKnowledge": "AI Knowledge"
|
||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
"meetings": "Termine",
|
"meetings": "Termine",
|
||||||
@@ -25,14 +25,15 @@
|
|||||||
"klaeger": "Kläger",
|
"klaeger": "Kläger",
|
||||||
"beklagte": "Beklagte",
|
"beklagte": "Beklagte",
|
||||||
"vmhMietverhltnises": "Mietverhältnisse",
|
"vmhMietverhltnises": "Mietverhältnisse",
|
||||||
|
"kuendigungen": "Kündigungen",
|
||||||
"contactsRumungsklage": "Freigegebene Nutzer",
|
"contactsRumungsklage": "Freigegebene Nutzer",
|
||||||
"dokumentesvmhraumungsklage": "Dokumente",
|
"dokumentesvmhraumungsklage": "Dokumente",
|
||||||
"freigeschalteteNutzer": "Freigeschaltete Nutzer (veraltet)",
|
"freigeschalteteNutzer": "Freigeschaltete Nutzer (veraltet)",
|
||||||
"collaborators": "Mitarbeiter",
|
"collaborators": "Mitarbeiter",
|
||||||
"vmhVermietersRKL": "Vermieter",
|
"vmhVermietersRKL": "Vermieter",
|
||||||
"pulse": "Pulse",
|
"pulse": "Pulse",
|
||||||
"cAdvowareAkte": "Advoware-Akte",
|
"advowareAkten": "Advoware Akten",
|
||||||
"cAICollection": "AI Collection"
|
"aIKnowledge": "AI Knowledge"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CVmhRumungsklage": "Räumungsklage erstellen"
|
"Create CVmhRumungsklage": "Räumungsklage erstellen"
|
||||||
|
|||||||
@@ -4,5 +4,8 @@
|
|||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Log": "Log"
|
"Log": "Log"
|
||||||
|
},
|
||||||
|
"scopeNamesPlural": {
|
||||||
|
"CAdvowareAkten": "Advoware Akten"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"links": {
|
|
||||||
"meetings": "Συναντήσεις",
|
|
||||||
"calls": "Κλήσεις",
|
|
||||||
"tasks": "Εργασίες"
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"Create CAICollection": "Δημιουργία AI Collection"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledge": "Δημιουργία AI Knowledge"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,6 @@
|
|||||||
"tasks": "Εργασίες"
|
"tasks": "Εργασίες"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CAdvowareAkten": "Δημιουργία Advoware Akte"
|
"Create CAdvowareAkten": "Δημιουργία Advoware Akten"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"fields": {
|
|
||||||
"cAdvowareAkte": "Advoware File",
|
|
||||||
"cAdvowareAkteId": "Advoware File ID",
|
|
||||||
"cAdvowareAkteName": "Advoware File Name",
|
|
||||||
"xaiCollectionId": "x.AI Collection ID",
|
|
||||||
"syncStatus": "Sync Status",
|
|
||||||
"lastSync": "Last Sync",
|
|
||||||
"cmietinkasso": "Mietinkasso",
|
|
||||||
"cmietinkassoId": "Mietinkasso ID",
|
|
||||||
"cmietinkassoName": "Mietinkasso Name",
|
|
||||||
"cvmhRumungsklage": "Räumungsklage",
|
|
||||||
"cvmhRumungsklageId": "Räumungsklage ID",
|
|
||||||
"cvmhRumungsklageName": "Räumungsklage Name"
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"meetings": "Meetings",
|
|
||||||
"calls": "Calls",
|
|
||||||
"tasks": "Tasks",
|
|
||||||
"cDokumente": "Documents",
|
|
||||||
"cAdvowareAkte": "Advoware File",
|
|
||||||
"cmietinkasso": "Mietinkasso",
|
|
||||||
"cvmhRumungsklage": "Räumungsklage"
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"Create CAICollection": "Create AI Collection"
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"xaiCollectionId": "Collection ID for x.AI synchronization",
|
|
||||||
"syncStatus": "x.AI synchronization status: pending_sync = Waiting for sync, clean = successfully synchronized, unclean = changes pending, failed = error, no_sync = Not synchronized",
|
|
||||||
"lastSync": "Time of last synchronization with x.AI"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"syncStatus": {
|
|
||||||
"pending_sync": "Waiting for Sync",
|
|
||||||
"clean": "Synchronized",
|
|
||||||
"unclean": "Changes Pending",
|
|
||||||
"failed": "Failed",
|
|
||||||
"no_sync": "No Sync"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"labels": {
|
|
||||||
"Create CAICollectionCDokumente": "Create AI Collection Document Link",
|
|
||||||
"CAICollectionCDokumente": "AI Collection Document Links"
|
|
||||||
},
|
|
||||||
"fields": {
|
|
||||||
"cAICollection": "AI Collection",
|
|
||||||
"cAICollectionId": "AI Collection ID",
|
|
||||||
"cDokumente": "Document",
|
|
||||||
"cDokumenteId": "Document ID",
|
|
||||||
"xaifileid": "XAI File ID",
|
|
||||||
"syncStatus": "Sync Status",
|
|
||||||
"deleted": "Deleted"
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"cAICollection": "AI Collection",
|
|
||||||
"cDokumente": "Document"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"syncStatus": {
|
|
||||||
"new": "New",
|
|
||||||
"changed": "Changed",
|
|
||||||
"synced": "Synced",
|
|
||||||
"deleted": "Deleted"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"xaifileid": "External XAI file ID for this document",
|
|
||||||
"syncStatus": "Synchronization status with XAI"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
49
custom/Espo/Custom/Resources/i18n/en_US/CAIKnowledge.json
Normal file
49
custom/Espo/Custom/Resources/i18n/en_US/CAIKnowledge.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"dokumentes": "Dokumente",
|
||||||
|
"vmhRumungsklage": "Räumungsklage",
|
||||||
|
"mietinkasso": "Mietinkasso",
|
||||||
|
"datenbankId": "Database ID",
|
||||||
|
"syncStatus": "Sync Status",
|
||||||
|
"lastSync": "Last Synchronization",
|
||||||
|
"aktivierungsstatus": "Activation Status",
|
||||||
|
"dokumenteAiDocumentId": "AI Document ID",
|
||||||
|
"dokumenteSyncstatus": "Sync Status",
|
||||||
|
"dokumenteLastSync": "Last Sync",
|
||||||
|
"dokumenteSyncedHash": "Sync Hash"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"dokumentes": "Dokumente",
|
||||||
|
"vmhRumungsklage": "Räumungsklage",
|
||||||
|
"mietinkasso": "Mietinkasso"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledge": "Create AI Knowledge"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"syncStatus": {
|
||||||
|
"synced": "Synchronized",
|
||||||
|
"unclean": "Not Synchronized",
|
||||||
|
"pending_sync": "Synchronization Pending"
|
||||||
|
},
|
||||||
|
"aktivierungsstatus": {
|
||||||
|
"new": "New",
|
||||||
|
"active": "Active",
|
||||||
|
"paused": "Paused",
|
||||||
|
"deactivated": "Deactivated"
|
||||||
|
},
|
||||||
|
"dokumenteSyncstatus": {
|
||||||
|
"new": "New",
|
||||||
|
"unclean": "Not Synchronized",
|
||||||
|
"synced": "Synchronized",
|
||||||
|
"failed": "Failed",
|
||||||
|
"unsupported": "Unsupported"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"syncStatus": "Global synchronization status: synced = All documents synchronized, unclean = At least one document is new or has changes, pending_sync = Synchronization started but not yet completed. Updated automatically based on document status.",
|
||||||
|
"lastSync": "Timestamp of the last successful synchronization of all documents",
|
||||||
|
"aktivierungsstatus": "Activation status of the AI Knowledge entry: new = Newly created, active = Actively synchronized, paused = Synchronization paused, deactivated = Synchronization deactivated",
|
||||||
|
"datenbankId": "Unique ID in the AI database"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"cAIKnowledge": "AI Knowledge",
|
||||||
|
"cDokumente": "Document",
|
||||||
|
"aiDocumentId": "AI Document ID",
|
||||||
|
"syncstatus": "Sync Status",
|
||||||
|
"lastSync": "Last Sync",
|
||||||
|
"syncedHash": "Synced Hash"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"cAIKnowledge": "AI Knowledge",
|
||||||
|
"cDokumente": "Document"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledgeCDokumente": "Create Link"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"syncstatus": {
|
||||||
|
"new": "New",
|
||||||
|
"unclean": "Changed",
|
||||||
|
"synced": "Synced",
|
||||||
|
"failed": "Failed",
|
||||||
|
"unsupported": "Unsupported"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"aiDocumentId": "External AI document reference ID",
|
||||||
|
"syncedHash": "Hash value of last synced document state"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"cAIKnowledge": "AI Knowledge",
|
||||||
|
"cDokumente": "Document",
|
||||||
|
"aiDocumentId": "AI Document ID",
|
||||||
|
"syncstatus": "Sync Status",
|
||||||
|
"lastSync": "Last Sync",
|
||||||
|
"syncedHash": "Synced Hash"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"cAIKnowledge": "AI Knowledge",
|
||||||
|
"cDokumente": "Document"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledgeCDokumente": "Create Link"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"syncstatus": {
|
||||||
|
"new": "New",
|
||||||
|
"unclean": "Changed",
|
||||||
|
"synced": "Synced",
|
||||||
|
"failed": "Failed",
|
||||||
|
"unsupported": "Unsupported"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"aiDocumentId": "External AI document reference ID",
|
||||||
|
"syncedHash": "Hash value of last synced document state"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +1,50 @@
|
|||||||
{
|
{
|
||||||
"fields": {
|
"fields": {
|
||||||
"advowareAktenzeichen": "Advoware Case Number",
|
"vmhRumungsklage": "Räumungsklagen",
|
||||||
"aktennr": "Advoware Identifier",
|
"mietinkasso": "Mietinkasso",
|
||||||
|
"aktenzeichen": "Aktenzeichen",
|
||||||
|
"aktennummer": "Aktennummer",
|
||||||
|
"aktenpfad": "File Path (Windows)",
|
||||||
"syncStatus": "Sync Status",
|
"syncStatus": "Sync Status",
|
||||||
"lastSync": "Last Sync",
|
"lastSync": "Last Synchronization",
|
||||||
"cmietinkasso": "Mietinkasso",
|
"aktivierungsstatus": "Activation Status",
|
||||||
"cmietinkassoId": "Mietinkasso ID",
|
"dokumentes": "Dokumente",
|
||||||
"cmietinkassoName": "Mietinkasso Name",
|
"dokumenteHnr": "HNR",
|
||||||
"cvmhRumungsklage": "Räumungsklage",
|
"dokumenteSyncstatus": "Sync Status",
|
||||||
"cvmhRumungsklageId": "Räumungsklage ID",
|
"dokumenteLastSync": "Last Sync",
|
||||||
"cvmhRumungsklageName": "Räumungsklage Name"
|
"dokumenteSyncedHash": "Sync Hash"
|
||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
"meetings": "Meetings",
|
"meetings": "Meetings",
|
||||||
"calls": "Calls",
|
"calls": "Calls",
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"cDokumente": "Documents",
|
"vmhRumungsklage": "Räumungsklagen",
|
||||||
"cAICollection": "AI Collection",
|
"mietinkasso": "Mietinkasso",
|
||||||
"cmietinkasso": "Mietinkasso",
|
"kuendigungen": "Terminations",
|
||||||
"cvmhRumungsklage": "Räumungsklage"
|
"dokumentes": "Dokumente"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CAdvowareAkten": "Create Advoware Akte"
|
"Create CAdvowareAkten": "Create Advoware Akten"
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"advowareAktenzeichen": "Case number from Advoware system",
|
|
||||||
"aktennr": "Unique case number from Advoware",
|
|
||||||
"syncStatus": "Advoware synchronization status: pending_sync = Waiting for sync, clean = successfully synchronized, unclean = changes pending, failed = error, no_sync = Not synchronized",
|
|
||||||
"lastSync": "Time of last synchronization with Advoware"
|
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"syncStatus": {
|
"syncStatus": {
|
||||||
"pending_sync": "Waiting for Sync",
|
"synced": "Synchronized",
|
||||||
"clean": "Synchronized",
|
"unclean": "Not Synchronized",
|
||||||
"unclean": "Changes Pending",
|
"pending_sync": "Synchronization Pending",
|
||||||
"failed": "Failed",
|
"failed": "Failed"
|
||||||
"no_sync": "No Sync"
|
},
|
||||||
|
"aktivierungsstatus": {
|
||||||
|
"new": "New",
|
||||||
|
"import": "Import",
|
||||||
|
"active": "Active",
|
||||||
|
"paused": "Paused",
|
||||||
|
"deactivated": "Deactivated"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"syncStatus": "Global synchronization status: synced = All documents synchronized, unclean = At least one document is new or has changes, pending_sync = Synchronization started but not yet completed, failed = Synchronization failed. Updated automatically based on document status.",
|
||||||
|
"lastSync": "Timestamp of the last successful synchronization of all documents",
|
||||||
|
"aktivierungsstatus": "Activation status of the file: new = Newly created, import = Imported from Advoware, active = Actively synchronized, paused = Synchronization paused, deactivated = Synchronization deactivated",
|
||||||
|
"aktenpfad": "Windows file path to the file in Advoware"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"cDokumenteId": "Document ID",
|
"cDokumenteId": "Document ID",
|
||||||
"hnr": "HNR",
|
"hnr": "HNR",
|
||||||
"syncStatus": "Sync Status",
|
"syncStatus": "Sync Status",
|
||||||
|
"syncedHash": "Sync Hash",
|
||||||
"deleted": "Deleted"
|
"deleted": "Deleted"
|
||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"hnr": "Advoware HNR reference for this document",
|
"hnr": "Advoware HNR reference for this document",
|
||||||
"syncStatus": "Synchronization status with Advoware"
|
"syncStatus": "Synchronization status with Advoware",
|
||||||
|
"syncedHash": "Hash value of the last synchronized document state (for change detection)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,32 @@
|
|||||||
{
|
{
|
||||||
"fields": {
|
"fields": {
|
||||||
"dokument": "Download",
|
"dokument": "Download",
|
||||||
"ydocumentuuid": "Y-Document-UUID",
|
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
|
"cAdvowareAkten": "Advoware File",
|
||||||
|
"cAdvowareAktenId": "Advoware File ID",
|
||||||
|
"cAdvowareAktenName": "Advoware File Name",
|
||||||
|
"hnr": "HNR (Advoware)",
|
||||||
|
"syncStatus": "Sync Status",
|
||||||
|
"syncedHash": "Sync Hash",
|
||||||
|
"usn": "USN",
|
||||||
|
"dateipfad": "File Path",
|
||||||
"contactsvmhdokumente": "Portal Users",
|
"contactsvmhdokumente": "Portal Users",
|
||||||
"vmhMietverhltnisesDokumente": "Tenancies",
|
"vmhMietverhltnisesDokumente": "Tenancies",
|
||||||
"vmhErstgespraechsdokumente": "Initial Consultations",
|
"vmhErstgespraechsdokumente": "Initial Consultations",
|
||||||
"vmhRumungsklagesdokumente": "Eviction Lawsuits",
|
"vmhRumungsklagesdokumente": "Eviction Lawsuits",
|
||||||
"kuendigungDokumente": "Terminations",
|
"kuendigungDokumente": "Terminations",
|
||||||
"md5sum": "MD5 Checksum",
|
"blake3hash": "Blake3 Hash",
|
||||||
"sha256": "SHA256 Checksum",
|
|
||||||
"beteiligte2dokumente": "Parties",
|
"beteiligte2dokumente": "Parties",
|
||||||
"mietobjekt2dokumente": "Properties",
|
"mietobjekt2dokumente": "Properties",
|
||||||
"mietinkassosdokumente": "Rent Collection",
|
"mietinkassosdokumente": "Rent Collection",
|
||||||
"kndigungensdokumente": "Terminations",
|
"kndigungensdokumente": "Terminations",
|
||||||
"aktennr": "Advoware Identifier",
|
"aIKnowledges": "AI Knowledge",
|
||||||
"advowareLastSync": "Advoware Last Sync",
|
"aiKnowledgeAiDocumentId": "AI Document ID",
|
||||||
"syncStatus": "Sync Status",
|
"aiKnowledgeSyncstatus": "AI Sync Status",
|
||||||
"xaiId": "x.AI ID",
|
"aiKnowledgeLastSync": "AI Last Sync"
|
||||||
"xaiCollections": "x.AI Collections",
|
|
||||||
"xaiSyncStatus": "Sync Status",
|
|
||||||
"fileStatus": "File Status"
|
|
||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
|
"cAdvowareAkten": "Advoware File",
|
||||||
"contactsvmhdokumente": "Portal Users",
|
"contactsvmhdokumente": "Portal Users",
|
||||||
"vmhMietverhltnisesDokumente": "Tenancies",
|
"vmhMietverhltnisesDokumente": "Tenancies",
|
||||||
"vmhErstgespraechsdokumente": "Initial Consultations",
|
"vmhErstgespraechsdokumente": "Initial Consultations",
|
||||||
@@ -32,43 +36,31 @@
|
|||||||
"mietobjekt2dokumente": "Properties",
|
"mietobjekt2dokumente": "Properties",
|
||||||
"mietinkassosdokumente": "Rent Collection",
|
"mietinkassosdokumente": "Rent Collection",
|
||||||
"kndigungensdokumente": "Terminations",
|
"kndigungensdokumente": "Terminations",
|
||||||
"cAICollections": "AI Collections",
|
"aIKnowledges": "AI Knowledge"
|
||||||
"cAdvowareAkten": "Advoware Files"
|
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CDokumente": "Create Dokument"
|
"Create CDokumente": "Create Dokument"
|
||||||
},
|
},
|
||||||
"layouts": {
|
"layouts": {
|
||||||
"listRaeumungsKl": "List (RaeumungsKl)"
|
"listRaeumungsKl": "List (RaeumungsKl)",
|
||||||
|
"listForAdvowareAkten": "List for Advoware Akten",
|
||||||
|
"listForAIKnowledge": "List for AI Knowledge"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"aktennr": "Unique document number from Advoware",
|
"blake3hash": "Cryptographic Blake3 hash of the file (faster and more secure than MD5/SHA256)",
|
||||||
"advowareLastSync": "Time of last synchronization with Advoware",
|
"hnr": "Hierarchical reference number in Advoware",
|
||||||
"syncStatus": "Advoware synchronization status: pending_sync = Waiting for sync, clean = successfully synchronized, unclean = changes pending, failed = error, no_sync = Not synchronized",
|
"syncStatus": "Sync status with Advoware: new=new, unclean=changed, synced=synchronized, failed=error, unsupported=not supported",
|
||||||
"xaiId": "Unique ID for x.AI synchronization",
|
"syncedHash": "Hash value at last successful synchronization",
|
||||||
"xaiCollections": "List of x.AI collections for this document",
|
"usn": "Update Sequence Number - Version number for synchronization",
|
||||||
"xaiSyncStatus": "x.AI synchronization status: pending_sync = Waiting for sync, clean = successfully synchronized, unclean = changes pending, failed = error, no_sync = Not synchronized",
|
"dateipfad": "Windows file path of the document in Advoware"
|
||||||
"fileStatus": "File status: new = newly uploaded, changed = modified, synced = synchronized"
|
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"syncStatus": {
|
"syncStatus": {
|
||||||
"pending_sync": "Waiting for Sync",
|
|
||||||
"clean": "Synchronized",
|
|
||||||
"unclean": "Changes Pending",
|
|
||||||
"failed": "Failed",
|
|
||||||
"no_sync": "No Sync"
|
|
||||||
},
|
|
||||||
"xaiSyncStatus": {
|
|
||||||
"pending_sync": "Waiting for Sync",
|
|
||||||
"clean": "Synchronized",
|
|
||||||
"unclean": "Changes Pending",
|
|
||||||
"failed": "Failed",
|
|
||||||
"no_sync": "No Sync"
|
|
||||||
},
|
|
||||||
"fileStatus": {
|
|
||||||
"new": "New",
|
"new": "New",
|
||||||
"changed": "Changed",
|
"unclean": "Changed",
|
||||||
"synced": "Synchronized"
|
"synced": "Synchronized",
|
||||||
|
"failed": "Failed",
|
||||||
|
"unsupported": "Unsupported"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"modifiedBy": "Modified By",
|
"modifiedBy": "Modified By",
|
||||||
"freigeschalteteNutzer": "Authorized Users",
|
"freigeschalteteNutzer": "Authorized Users",
|
||||||
"collaborators": "Collaborators",
|
"collaborators": "Collaborators",
|
||||||
"advowareAktenzeichen": "Advoware File Number",
|
"advowareAkten": "Advoware File",
|
||||||
"aktennr": "Case Number",
|
"aktennr": "Case Number",
|
||||||
"advowareLastSync": "Last Sync",
|
"advowareLastSync": "Last Sync",
|
||||||
"syncStatus": "Sync Status",
|
"syncStatus": "Sync Status",
|
||||||
@@ -44,6 +44,8 @@
|
|||||||
"gekuendigte": "Tenant",
|
"gekuendigte": "Tenant",
|
||||||
"dokumenteskuendigung": "Documents",
|
"dokumenteskuendigung": "Documents",
|
||||||
"contactsKuendigung": "Portal Access",
|
"contactsKuendigung": "Portal Access",
|
||||||
|
"advowareAkten": "Advoware Case File",
|
||||||
|
"vmhRumungsklages": "Eviction Lawsuits",
|
||||||
"pulse": "Pulses"
|
"pulse": "Pulses"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
@@ -103,7 +105,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"advowareAktenzeichen": "File number from Advoware for synchronization",
|
|
||||||
"aktennr": "Case number from Advoware",
|
"aktennr": "Case number from Advoware",
|
||||||
"syncStatus": "Advoware synchronization status: pending_sync = Waiting for sync, clean = successfully synchronized, unclean = changes pending, failed = error, no_sync = Not synchronized",
|
"syncStatus": "Advoware synchronization status: pending_sync = Waiting for sync, clean = successfully synchronized, unclean = changes pending, failed = error, no_sync = Not synchronized",
|
||||||
"sendungsverfolgungsnummer": "Tracking number for registered mail",
|
"sendungsverfolgungsnummer": "Tracking number for registered mail",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"vmhMietverhltnises": "Tenancies",
|
"vmhMietverhltnises": "Tenancies",
|
||||||
"contactsMietinkasso": "Portal Users",
|
"contactsMietinkasso": "Portal Users",
|
||||||
"dokumentesmietinkasso": "Documents",
|
"dokumentesmietinkasso": "Documents",
|
||||||
"gerichtsrubrum": "Court Rubrum",
|
|
||||||
"gegenstandswert": "Claim Value",
|
"gegenstandswert": "Claim Value",
|
||||||
"kuendigungsservice": "Termination Service",
|
"kuendigungsservice": "Termination Service",
|
||||||
"aussergerichtlicheGebuehren13": "Out-of-court Fees 1.3",
|
"aussergerichtlicheGebuehren13": "Out-of-court Fees 1.3",
|
||||||
@@ -14,8 +13,8 @@
|
|||||||
"freigeschalteteNutzer": "Activated Users (deprecated)",
|
"freigeschalteteNutzer": "Activated Users (deprecated)",
|
||||||
"collaborators": "Collaborators",
|
"collaborators": "Collaborators",
|
||||||
"vmhVermietersMIK": "Landlord",
|
"vmhVermietersMIK": "Landlord",
|
||||||
"cAdvowareAkte": "Advoware File",
|
"advowareAkten": "Advoware Akten",
|
||||||
"cAICollection": "AI Collection"
|
"aIKnowledge": "AI Knowledge"
|
||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
"meetings": "Meetings",
|
"meetings": "Meetings",
|
||||||
@@ -30,14 +29,13 @@
|
|||||||
"collaborators": "Collaborators",
|
"collaborators": "Collaborators",
|
||||||
"vmhVermietersMIK": "Landlord",
|
"vmhVermietersMIK": "Landlord",
|
||||||
"pulse": "Pulses",
|
"pulse": "Pulses",
|
||||||
"cAdvowareAkte": "Advoware File",
|
"advowareAkten": "Advoware Akten",
|
||||||
"cAICollection": "AI Collection"
|
"aIKnowledge": "AI Knowledge"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CMietinkasso": "Create Mietinkasso"
|
"Create CMietinkasso": "Create Mietinkasso"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"gerichtsrubrum": "Court proceeding rubrum",
|
|
||||||
"gegenstandswert": "Value of the disputed matter",
|
"gegenstandswert": "Value of the disputed matter",
|
||||||
"kuendigungsservice": "Termination service enabled"
|
"kuendigungsservice": "Termination service enabled"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,15 @@
|
|||||||
"aussergerichtlicheGebuehren13": "Out-of-Court Fees 1.3",
|
"aussergerichtlicheGebuehren13": "Out-of-Court Fees 1.3",
|
||||||
"gerichtskosten1Instanz": "Court Costs 1st Instance",
|
"gerichtskosten1Instanz": "Court Costs 1st Instance",
|
||||||
"anwaltskosten1Instanz": "Attorney Fees 1st Instance",
|
"anwaltskosten1Instanz": "Attorney Fees 1st Instance",
|
||||||
"cAdvowareAkte": "Advoware File",
|
"advowareAkten": "Advoware Akten",
|
||||||
"cAICollection": "AI Collection"
|
"aIKnowledge": "AI Knowledge"
|
||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
"meetings": "Meetings",
|
"meetings": "Meetings",
|
||||||
"calls": "Calls",
|
"calls": "Calls",
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"vmhMietverhltnises": "Tenancies",
|
"vmhMietverhltnises": "Tenancies",
|
||||||
|
"kuendigungen": "Terminations",
|
||||||
"freigeschalteteNutzer": "Activated Users",
|
"freigeschalteteNutzer": "Activated Users",
|
||||||
"collaborators": "Collaborators",
|
"collaborators": "Collaborators",
|
||||||
"vmhVermietersRKL": "Landlord",
|
"vmhVermietersRKL": "Landlord",
|
||||||
@@ -29,8 +30,8 @@
|
|||||||
"klaeger": "Plaintiff",
|
"klaeger": "Plaintiff",
|
||||||
"contactsRumungsklage": "Portal Users",
|
"contactsRumungsklage": "Portal Users",
|
||||||
"pulse": "Pulses",
|
"pulse": "Pulses",
|
||||||
"cAdvowareAkte": "Advoware File",
|
"advowareAkten": "Advoware Akten",
|
||||||
"cAICollection": "AI Collection"
|
"aIKnowledge": "AI Knowledge"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CVmhRumungsklage": "Create Räumungsklage"
|
"Create CVmhRumungsklage": "Create Räumungsklage"
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
"CMietinkasso": "Mietinkasso",
|
"CMietinkasso": "Mietinkasso",
|
||||||
"CKuendigung": "Kündigung",
|
"CKuendigung": "Kündigung",
|
||||||
"CPuls": "Puls",
|
"CPuls": "Puls",
|
||||||
"CAICollection": "AI Collection",
|
"CAdvowareAkten": "Advoware Akten",
|
||||||
"CAdvowareAkten": "Advoware Akte"
|
"CAIKnowledge": "AI Knowledge"
|
||||||
},
|
},
|
||||||
"scopeNamesPlural": {
|
"scopeNamesPlural": {
|
||||||
"CVmhMietverhltnis": "Mietverhältnisse",
|
"CVmhMietverhltnis": "Mietverhältnisse",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"CMietinkasso": "Mietinkassa",
|
"CMietinkasso": "Mietinkassa",
|
||||||
"CKuendigung": "Kündigungen",
|
"CKuendigung": "Kündigungen",
|
||||||
"CPuls": "Pulse",
|
"CPuls": "Pulse",
|
||||||
"CAICollection": "AI Collections",
|
"CAdvowareAkten": "Advoware Akten",
|
||||||
"CAdvowareAkten": "Advoware Akten"
|
"CAIKnowledge": "AI Knowledge"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"links": {
|
|
||||||
"meetings": "Reuniones",
|
|
||||||
"calls": "Llamadas",
|
|
||||||
"tasks": "Tareas"
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"Create CAICollection": "Crear AI Collection"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledge": "Crear AI Knowledge"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,6 @@
|
|||||||
"tasks": "Tareas"
|
"tasks": "Tareas"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CAdvowareAkten": "Crear Advoware Akte"
|
"Create CAdvowareAkten": "Crear Advoware Akten"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"links": {
|
|
||||||
"meetings": "Presentaciones",
|
|
||||||
"calls": "Llamadas",
|
|
||||||
"tasks": "Tareas"
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"Create CAICollection": "Crear AI Collection"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledge": "Crear AI Knowledge"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,6 @@
|
|||||||
"tasks": "Tareas"
|
"tasks": "Tareas"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CAdvowareAkten": "Crear Advoware Akte"
|
"Create CAdvowareAkten": "Crear Advoware Akten"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"links": {
|
|
||||||
"meetings": "جلسات",
|
|
||||||
"calls": "تماس ها",
|
|
||||||
"tasks": "وظایف"
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"Create CAICollection": "ایجاد AI Collection"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user