diff --git a/files/custom/Espo/Modules/SmartAssistant/Controllers/SmartAssistant.php b/files/custom/Espo/Modules/SmartAssistant/Controllers/SmartAssistant.php index 4071cfd..ae4349c 100644 --- a/files/custom/Espo/Modules/SmartAssistant/Controllers/SmartAssistant.php +++ b/files/custom/Espo/Modules/SmartAssistant/Controllers/SmartAssistant.php @@ -12,6 +12,7 @@ use Espo\Modules\SmartAssistant\Services\SmartAssistantService; use Espo\Modules\SmartAssistant\Services\ActionExecutor; use Espo\Modules\SmartAssistant\Services\CaseMemoryService; use Espo\Modules\SmartAssistant\Services\DocumentAnalyzer; +use Espo\Modules\SmartAssistant\Services\GenericTemplateGenerator; use Espo\Entities\User; class SmartAssistant @@ -277,4 +278,27 @@ class SmartAssistant 'results' => $results, ]; } + + public function postActionGenerateFromTemplate(Request $request, Response $response): array + { + $this->checkAccess(); + $data = $request->getParsedBody(); + + $templateId = $data->templateId ?? null; + $caseId = $data->caseId ?? null; + + if (empty($templateId)) { + throw new BadRequest('templateId is required.'); + } + if (empty($caseId)) { + throw new BadRequest('caseId is required.'); + } + + $documentSubject = $data->documentSubject ?? null; + $customPlaceholders = (array) ($data->customPlaceholders ?? []); + + $generator = $this->injectableFactory->create(GenericTemplateGenerator::class); + + return $generator->generate($templateId, $caseId, $documentSubject, $customPlaceholders); + } } diff --git a/files/custom/Espo/Modules/SmartAssistant/Services/GenericTemplateGenerator.php b/files/custom/Espo/Modules/SmartAssistant/Services/GenericTemplateGenerator.php new file mode 100644 index 0000000..0fcaab6 --- /dev/null +++ b/files/custom/Espo/Modules/SmartAssistant/Services/GenericTemplateGenerator.php @@ -0,0 +1,213 @@ +entityManager->getEntityById('DocumentTemplate', $templateId); + if (!$template) { + throw new NotFound("Template {$templateId} not found."); + } + + if (!$template->get('isActive')) { + throw new Error("Template is not active."); + } + + $templatePath = $template->get('templatePath'); + if (!$templatePath) { + throw new Error("Template path is not configured."); + } + + $case = $this->entityManager->getEntityById('Case', $caseId); + if (!$case) { + throw new NotFound("Case {$caseId} not found."); + } + + $templateName = $template->get('name') ?? 'document'; + + // Step 1: Download template from WebDAV + $localTemplatePath = $this->downloadTemplate($templatePath); + + try { + // Step 2: Build placeholder data from entity fields + $templateService = $this->injectableFactory->create(DocumentTemplateService::class); + $placeholderData = $templateService->buildPlaceholderData($case); + + // Step 3: Merge custom placeholders (override entity data) + foreach ($customPlaceholders as $key => $value) { + $placeholderData[$key] = $value; + } + + // Step 4: Process template + $outputPath = self::TEMP_DIR . uniqid('gen_') . '.docx'; + if (!is_dir(self::TEMP_DIR)) { + mkdir(self::TEMP_DIR, 0775, true); + } + + $this->processTemplate($localTemplatePath, $outputPath, $placeholderData); + + // Step 5: Read generated content + $content = file_get_contents($outputPath); + @unlink($outputPath); + @unlink($localTemplatePath); + + // Step 6: Build document name + $contactName = $this->getContactName($case); + $docName = $documentSubject ?? "{$templateName} — {$contactName}"; + + // Step 7: Create Attachment + Document + $attachment = $this->entityManager->getNewEntity('Attachment'); + $attachment->set([ + 'name' => $docName . '.docx', + 'type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'role' => 'Attachment', + 'size' => strlen($content), + 'relatedType' => 'Document', + ]); + $this->entityManager->saveEntity($attachment); + file_put_contents('data/upload/' . $attachment->getId(), $content); + + $document = $this->entityManager->getNewEntity('Document'); + $document->set([ + 'name' => $docName, + 'fileId' => $attachment->getId(), + 'fileName' => $docName . '.docx', + 'assignedUserId' => $this->user->getId(), + ]); + $this->entityManager->saveEntity($document); + + // Link to Case + $this->entityManager->getRDBRepository('Case') + ->getRelation($case, 'documents') + ->relate($document); + + $this->log->info("GenericTemplateGenerator: Created '{$docName}' from template '{$templateName}' for Case {$caseId}"); + + return [ + 'success' => true, + 'message' => "✅ מסמך \"{$docName}\" נוצר בהצלחה מתבנית \"{$templateName}\"\n📄 המסמך צורף לתיק.", + 'entityType' => 'Document', + 'entityId' => $document->getId(), + ]; + } catch (\Exception $e) { + if (isset($localTemplatePath) && file_exists($localTemplatePath)) { + @unlink($localTemplatePath); + } + throw $e; + } + } + + private function downloadTemplate(string $templatePath): string + { + // Try local paths first + if (file_exists($templatePath)) { + return $templatePath; + } + + $localPath = 'data/document-templates/' . ltrim($templatePath, '/'); + if (file_exists($localPath)) { + return $localPath; + } + + // Download from NetworkStorage (WebDAV) + try { + $nds = $this->injectableFactory->create(NetworkDocumentService::class); + $client = $nds->getClient(); + + $integration = $this->entityManager->getEntityById('Integration', 'NetworkStorage'); + $templatesFolderPath = 'Templates'; + if ($integration && $integration->get('enabled')) { + $data = (array) ($integration->get('data') ?? []); + $templatesFolderPath = $data['templatesFolderPath'] ?? 'Templates'; + } + + $storagePath = $templatesFolderPath . '/' . $templatePath; + + if (!$client->exists($storagePath)) { + throw new Error("Template file not found on storage: {$storagePath}"); + } + + $content = $client->downloadFile($storagePath); + + if (!is_dir(self::TEMP_DIR)) { + mkdir(self::TEMP_DIR, 0775, true); + } + $tmpPath = self::TEMP_DIR . 'tpl_' . uniqid() . '.docx'; + file_put_contents($tmpPath, $content); + + return $tmpPath; + } catch (Error $e) { + throw $e; + } catch (\Exception $e) { + throw new Error("Failed to download template: " . $e->getMessage()); + } + } + + private function processTemplate(string $inputPath, string $outputPath, array $data): void + { + if (!class_exists(TemplateProcessor::class, false)) { + $autoloader = 'vendor/phpoffice/phpword/src/PhpWord/Autoloader.php'; + if (file_exists($autoloader)) { + require_once $autoloader; + \PhpOffice\PhpWord\Autoloader::register(); + } + } + + $processor = new TemplateProcessor($inputPath); + + foreach ($data as $key => $value) { + if ($value === null) { + $value = ''; + } + if (is_array($value)) { + $value = implode(', ', $value); + } elseif (is_bool($value)) { + $value = $value ? 'כן' : 'לא'; + } + $processor->setValue($key, (string) $value); + } + + $processor->saveAs($outputPath); + } + + private function getContactName($case): string + { + $contactId = $case->get('contactId'); + if (!$contactId) { + $ids = $case->getLinkMultipleIdList('contacts'); + if (!empty($ids)) { + $contactId = $ids[0]; + } + } + + if ($contactId) { + $contact = $this->entityManager->getEntityById('Contact', $contactId); + if ($contact) { + return trim($contact->get('firstName') . ' ' . $contact->get('lastName')); + } + } + + return $case->get('name') ?? ''; + } +} diff --git a/manifest.json b/manifest.json index 9837202..38b9f0e 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "module": "SmartAssistant", "description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and AI Gateway integration", "author": "klear", - "version": "2.6.1", + "version": "2.7.0", "acceptableVersions": [ ">=8.0.0" ],