diff --git a/custom/Espo/Custom/Api/JunctionData/GetDokumentes.php b/custom/Espo/Custom/Api/JunctionData/GetDokumentes.php new file mode 100644 index 00000000..c06bb241 --- /dev/null +++ b/custom/Espo/Custom/Api/JunctionData/GetDokumentes.php @@ -0,0 +1,71 @@ +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 + ]); + } +} diff --git a/custom/Espo/Custom/Api/JunctionData/LinkDokument.php b/custom/Espo/Custom/Api/JunctionData/LinkDokument.php new file mode 100644 index 00000000..adb843b3 --- /dev/null +++ b/custom/Espo/Custom/Api/JunctionData/LinkDokument.php @@ -0,0 +1,178 @@ +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; + } +} diff --git a/custom/Espo/Custom/Api/JunctionData/UpdateJunction.php b/custom/Espo/Custom/Api/JunctionData/UpdateJunction.php new file mode 100644 index 00000000..c3866a92 --- /dev/null +++ b/custom/Espo/Custom/Api/JunctionData/UpdateJunction.php @@ -0,0 +1,123 @@ +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; + } +} diff --git a/custom/Espo/Custom/Resources/i18n/de_DE/CAIKnowledgeCDokumente.json b/custom/Espo/Custom/Resources/i18n/de_DE/CAIKnowledgeDokumente.json similarity index 54% rename from custom/Espo/Custom/Resources/i18n/de_DE/CAIKnowledgeCDokumente.json rename to custom/Espo/Custom/Resources/i18n/de_DE/CAIKnowledgeDokumente.json index 2a8fb06f..a5d76dff 100644 --- a/custom/Espo/Custom/Resources/i18n/de_DE/CAIKnowledgeCDokumente.json +++ b/custom/Espo/Custom/Resources/i18n/de_DE/CAIKnowledgeDokumente.json @@ -1,31 +1,30 @@ { - "labels": { - "Create CAIKnowledgeCDokumente": "AI Knowledge-Dokument Verknüpfung erstellen", - "CAIKnowledgeCDokumente": "AI Knowledge-Dokument Verknüpfung" - }, "fields": { "cAIKnowledge": "AI Knowledge", - "cAIKnowledgeId": "AI Knowledge ID", "cDokumente": "Dokument", - "cDokumenteId": "Dokument ID", "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": "Unklar", + "unclean": "Geändert", "synced": "Synchronisiert", - "failed": "Fehlgeschlagen", + "failed": "Fehler", "unsupported": "Nicht unterstützt" } }, "tooltips": { "aiDocumentId": "Externe AI-Dokument-Referenz-ID", - "syncstatus": "Status der Synchronisation mit externem AI-System", - "lastSync": "Zeitpunkt der letzten erfolgreichen Synchronisation", - "syncedHash": "Hash-Wert des zuletzt synchronisierten Dokument-Zustands (zur Änderungserkennung)" + "syncedHash": "Hash-Wert des zuletzt synchronisierten Dokument-Zustands" } } diff --git a/custom/Espo/Custom/Resources/i18n/en_US/CAIKnowledgeCDokumente.json b/custom/Espo/Custom/Resources/i18n/en_US/CAIKnowledgeCDokumente.json deleted file mode 100644 index 282d56be..00000000 --- a/custom/Espo/Custom/Resources/i18n/en_US/CAIKnowledgeCDokumente.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "labels": { - "Create CAIKnowledgeCDokumente": "Create AI Knowledge-Document Link", - "CAIKnowledgeCDokumente": "AI Knowledge-Document Link" - }, - "fields": { - "cAIKnowledge": "AI Knowledge", - "cAIKnowledgeId": "AI Knowledge ID", - "cDokumente": "Document", - "cDokumenteId": "Document ID", - "aiDocumentId": "AI Document ID", - "syncstatus": "Sync Status", - "lastSync": "Last Synchronization", - "syncedHash": "Sync Hash" - }, - "options": { - "syncstatus": { - "new": "New", - "unclean": "Unclean", - "synced": "Synced", - "failed": "Failed", - "unsupported": "Unsupported" - } - }, - "tooltips": { - "aiDocumentId": "External AI document reference ID", - "syncstatus": "Synchronization status with external AI system", - "lastSync": "Timestamp of the last successful synchronization", - "syncedHash": "Hash value of the last synchronized document state (for change detection)" - } -} diff --git a/custom/Espo/Custom/Resources/i18n/en_US/CAIKnowledgeDokumente.json b/custom/Espo/Custom/Resources/i18n/en_US/CAIKnowledgeDokumente.json new file mode 100644 index 00000000..983bb7fa --- /dev/null +++ b/custom/Espo/Custom/Resources/i18n/en_US/CAIKnowledgeDokumente.json @@ -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" + } +} diff --git a/custom/Espo/Custom/Resources/metadata/app/api.json b/custom/Espo/Custom/Resources/metadata/app/api.json index 3d053e3f..0d117f1d 100644 --- a/custom/Espo/Custom/Resources/metadata/app/api.json +++ b/custom/Espo/Custom/Resources/metadata/app/api.json @@ -9,6 +9,21 @@ "route": "/CPuls/:id/abschliessen-fuer-team", "method": "post", "actionClassName": "Espo\\Custom\\Api\\CPuls\\AbschliessenFuerTeam" + }, + { + "route": "/JunctionData/CAIKnowledge/:knowledgeId/dokumentes", + "method": "get", + "actionClassName": "Espo\\Custom\\Api\\JunctionData\\GetDokumentes" + }, + { + "route": "/JunctionData/CAIKnowledge/:knowledgeId/dokumentes/:documentId", + "method": "put", + "actionClassName": "Espo\\Custom\\Api\\JunctionData\\UpdateJunction" + }, + { + "route": "/JunctionData/CAIKnowledge/:knowledgeId/dokumentes/:documentId", + "method": "post", + "actionClassName": "Espo\\Custom\\Api\\JunctionData\\LinkDokument" } ] } diff --git a/custom/Espo/Custom/Resources/metadata/entityDefs/CAIKnowledgeCDokumente.json b/custom/Espo/Custom/Resources/metadata/entityDefs/CAIKnowledgeDokumente.json similarity index 59% rename from custom/Espo/Custom/Resources/metadata/entityDefs/CAIKnowledgeCDokumente.json rename to custom/Espo/Custom/Resources/metadata/entityDefs/CAIKnowledgeDokumente.json index 8041bb02..10cec143 100644 --- a/custom/Espo/Custom/Resources/metadata/entityDefs/CAIKnowledgeCDokumente.json +++ b/custom/Espo/Custom/Resources/metadata/entityDefs/CAIKnowledgeDokumente.json @@ -1,4 +1,5 @@ { + "table": "c_a_i_knowledge_dokumente", "fields": { "id": { "type": "id", @@ -6,58 +7,58 @@ "autoincrement": true }, "cAIKnowledge": { - "type": "link" + "type": "link", + "entity": "CAIKnowledge" }, "cAIKnowledgeId": { "type": "varchar", "len": 17, "index": true }, + "cAIKnowledgeName": { + "type": "varchar", + "notStorable": true, + "relation": "cAIKnowledge", + "foreign": "name" + }, "cDokumente": { - "type": "link" + "type": "link", + "entity": "CDokumente" }, "cDokumenteId": { "type": "varchar", "len": 17, "index": true }, + "cDokumenteName": { + "type": "varchar", + "notStorable": true, + "relation": "cDokumente", + "foreign": "name" + }, "aiDocumentId": { "type": "varchar", "len": 255, - "isCustom": true, "tooltip": true }, "syncstatus": { "type": "enum", - "required": false, - "options": [ - "new", - "unclean", - "synced", - "failed", - "unsupported" - ], + "options": ["new", "unclean", "synced", "failed", "unsupported"], + "default": "new", "style": { - "new": "info", + "new": "primary", "unclean": "warning", "synced": "success", "failed": "danger", "unsupported": "default" - }, - "default": "new", - "isCustom": true, - "tooltip": true + } }, "lastSync": { - "type": "datetime", - "required": false, - "isCustom": true, - "tooltip": true + "type": "datetime" }, "syncedHash": { "type": "varchar", "len": 64, - "isCustom": true, "tooltip": true }, "deleted": { @@ -68,30 +69,17 @@ "links": { "cAIKnowledge": { "type": "belongsTo", - "entity": "CAIKnowledge" + "entity": "CAIKnowledge", + "foreign": "dokumentes" }, "cDokumente": { "type": "belongsTo", - "entity": "CDokumente" + "entity": "CDokumente", + "foreign": "aIKnowledges" } }, "collection": { "orderBy": "id", - "order": "desc" - }, - "indexes": { - "cAIKnowledgeId": { - "columns": ["cAIKnowledgeId"] - }, - "cDokumenteId": { - "columns": ["cDokumenteId"] - }, - "syncstatus": { - "columns": ["syncstatus"] - }, - "uniqueRelation": { - "type": "unique", - "columns": ["cAIKnowledgeId", "cDokumenteId", "deleted"] - } + "order": "asc" } } diff --git a/custom/Espo/Custom/Resources/metadata/scopes/CAIKnowledgeCDokumente.json b/custom/Espo/Custom/Resources/metadata/scopes/CAIKnowledgeDokumente.json similarity index 53% rename from custom/Espo/Custom/Resources/metadata/scopes/CAIKnowledgeCDokumente.json rename to custom/Espo/Custom/Resources/metadata/scopes/CAIKnowledgeDokumente.json index f9c1f7a0..29dd859e 100644 --- a/custom/Espo/Custom/Resources/metadata/scopes/CAIKnowledgeCDokumente.json +++ b/custom/Espo/Custom/Resources/metadata/scopes/CAIKnowledgeDokumente.json @@ -1,10 +1,11 @@ { "entity": true, - "type": "Base", - "module": "Custom", - "object": true, - "isCustom": true, + "object": false, + "layouts": false, "tab": false, "acl": true, - "disabled": false + "customizable": false, + "type": "Base", + "module": "Custom", + "isCustom": true } diff --git a/custom/Espo/Custom/Resources/routes.json b/custom/Espo/Custom/Resources/routes.json new file mode 100644 index 00000000..90119c3b --- /dev/null +++ b/custom/Espo/Custom/Resources/routes.json @@ -0,0 +1,17 @@ +[ + { + "route": "/JunctionData/CAIKnowledge/:knowledgeId/dokumentes", + "method": "get", + "actionClassName": "Espo\\Custom\\Api\\JunctionData\\GetDokumentes" + }, + { + "route": "/JunctionData/CAIKnowledge/:knowledgeId/dokumentes/:documentId", + "method": "put", + "actionClassName": "Espo\\Custom\\Api\\JunctionData\\UpdateJunction" + }, + { + "route": "/JunctionData/CAIKnowledge/:knowledgeId/dokumentes/:documentId", + "method": "post", + "actionClassName": "Espo\\Custom\\Api\\JunctionData\\LinkDokument" + } +] diff --git a/custom/Espo/Custom/Services/CAIKnowledgeCDokumente.php b/custom/Espo/Custom/Services/CAIKnowledgeCDokumente.php deleted file mode 100644 index f6606564..00000000 --- a/custom/Espo/Custom/Services/CAIKnowledgeCDokumente.php +++ /dev/null @@ -1,14 +0,0 @@ - 'youtube.com', 1 => 'google.com' ], - 'microtime' => 1773348571.271434, + 'microtime' => 1773351315.93688, 'siteUrl' => 'https://crm.bitbylaw.com', 'fullTextSearchMinLength' => 4, 'webSocketUrl' => 'ws://api.bitbylaw.com:5000/espocrm/ws', diff --git a/data/state.php b/data/state.php index 74d48dc1..94a177fb 100644 --- a/data/state.php +++ b/data/state.php @@ -1,7 +1,7 @@ 1773348571, - 'microtimeState' => 1773348571.391315, + 'cacheTimestamp' => 1773351316, + 'microtimeState' => 1773351316.064287, 'currencyRates' => [ 'EUR' => 1.0 ],