Compare commits

..

4 Commits

Author SHA1 Message Date
chaim 14979dba4b fix: direct access report placeholder mismatches and missing recommendation option
- Fix cLegalAidNumber placeholder key mismatch (template expects cLegalAidNumber, code sent legalAidNumber)
- Add fallback for flat legalAnalysis fields (AI may send them outside the nested object)
- Improve assignedUserName resolution: prefer case assignee, filter out system/API user names
- Add "המשך ייצוג" positive recommendation option to enum, checkboxes, prompt, and DOCX template

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:30:19 +00:00
chaim 015586a9df fix: resolve report template from WebDAV when not found locally
DirectAccessReportGenerator now downloads the template from NetworkStorage
(WebDAV) if it's not found in data/document-templates/. This means template
updates via WebDAV are picked up automatically without manual file copy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:41:26 +00:00
chaim 3992384d96 fix: correct tool name in prompts from generate_initial_report to generate_direct_access_report
The AI prompt referenced the old tool name (generate_initial_report) but the
metadata and handler were registered as generate_direct_access_report, causing
404 errors when Shira tried to generate direct access reports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:06:06 +00:00
chaim cf523cbd86 fix: resolve template path and PHPWord autoload in DirectAccessReportGenerator
- Fix resolveTemplatePath to check data/document-templates/ prefix
- Add PHPWord autoloader fallback when not in composer autoload
- Fix Document-Case linking (use Case.documents relation side)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:24:17 +00:00
5 changed files with 116 additions and 28 deletions
@@ -58,9 +58,28 @@ class DirectAccessReportGenerator
? trim($contact->get('firstName') . ' ' . $contact->get('lastName'))
: 'ללא איש קשר';
// Get assigned user name
$assignedUser = $userId ? $this->entityManager->getEntityById('User', $userId) : null;
$assignedUserName = $assignedUser ? $assignedUser->get('name') : ($this->user->get('name') ?? '');
// Get assigned user name — prefer case's assignedUser, then userId, then current user
$assignedUserName = '';
$caseAssignedUserId = $case->get('assignedUserId');
if ($caseAssignedUserId) {
$caseAssignedUser = $this->entityManager->getEntityById('User', $caseAssignedUserId);
if ($caseAssignedUser) {
$assignedUserName = $caseAssignedUser->get('name') ?? '';
}
}
if (empty($assignedUserName) && $userId) {
$assignedUser = $this->entityManager->getEntityById('User', $userId);
if ($assignedUser) {
$assignedUserName = $assignedUser->get('name') ?? '';
}
}
if (empty($assignedUserName)) {
$assignedUserName = $this->user->get('name') ?? '';
}
// Filter out system/API user names
if (in_array(strtolower($assignedUserName), ['api', 'x-api-key', 'system', 'admin'], true)) {
$assignedUserName = '';
}
// Build placeholder data directly from params
$data = $this->buildPlaceholderData($params, $case, $contactName, $assignedUserName);
@@ -83,6 +102,11 @@ class DirectAccessReportGenerator
$content = file_get_contents($outputPath);
@unlink($outputPath);
// Clean up temp template if downloaded from storage
if (str_starts_with($templatePath, self::TEMP_DIR)) {
@unlink($templatePath);
}
// Create Attachment
$attachment = $this->entityManager->getNewEntity('Attachment');
$attachment->set([
@@ -106,10 +130,10 @@ class DirectAccessReportGenerator
]);
$this->entityManager->saveEntity($document);
// Link Document to Case
$this->entityManager->getRDBRepository('Document')
->getRelation($document, 'cases')
->relate($case);
// Link Document to Case (via Case side of relationship)
$this->entityManager->getRDBRepository('Case')
->getRelation($case, 'documents')
->relate($document);
$this->log->info("LegalAssistance: Report generated — Document {$document->getId()} attached to Case {$caseId}");
@@ -128,9 +152,18 @@ class DirectAccessReportGenerator
$legalAnalysis = json_decode($legalAnalysis, true) ?? [];
}
// Fallback: if AI sent legalAnalysis fields flat (not nested), pick them up
$laFields = ['q1DocsDetail', 'q2ReasonDetail', 'q4PanelComposition', 'q6ComplaintsDetail',
'q8ClinicalGap', 'q9MoharaDetail', 'q10RehabDetail', 'q11ScoreDetail', 'q12UniqueDetail'];
foreach ($laFields as $f) {
if (empty($legalAnalysis[$f]) && !empty($params[$f])) {
$legalAnalysis[$f] = $params[$f];
}
}
return [
// Header
'legalAidNumber' => $params['legalAidNumber'] ?? $case->get('cLegalAidNumber') ?? '',
'cLegalAidNumber' => $params['legalAidNumber'] ?? $case->get('cLegalAidNumber') ?? '',
'contactName' => $contactName,
'assignedUserName' => $assignedUserName,
'reportDate' => $this->formatDate(date('Y-m-d')),
@@ -206,6 +239,7 @@ class DirectAccessReportGenerator
$data['chk_contact_bad'] = ($contactStatus === 'לא תקין') ? $CHK : $UNCHK;
$rec = $params['recommendationType'] ?? '';
$data['chk_rec_proceed'] = ($rec === 'המשך ייצוג') ? $CHK : $UNCHK;
$data['chk_rec_consulting'] = ($rec === 'ייעוץ והדרכה חלף ייצוג') ? $CHK : $UNCHK;
$data['chk_rec_waiver'] = ($rec === 'ויתור הלקוח') ? $CHK : $UNCHK;
$data['chk_rec_replacement'] = ($rec === 'החלפת ייצוג') ? $CHK : $UNCHK;
@@ -230,32 +264,37 @@ class DirectAccessReportGenerator
private function resolveTemplatePath(): string
{
// Look up template via DocumentTemplate entity
$template = $this->entityManager
->getRDBRepository('DocumentTemplate')
->where(['name' => self::TEMPLATE_NAME])
->findOne();
if ($template) {
$fileId = $template->get('fileId');
if ($fileId) {
$filePath = 'data/upload/' . $fileId;
if (file_exists($filePath)) {
return $filePath;
}
}
// Try templatePath field
$templatePath = $template->get('templatePath');
if ($templatePath && file_exists($templatePath)) {
return $templatePath;
if ($templatePath) {
// Try as-is (absolute or relative)
if (file_exists($templatePath)) {
return $templatePath;
}
// Try under data/document-templates/
$resolved = 'data/document-templates/' . ltrim($templatePath, '/');
if (file_exists($resolved)) {
return $resolved;
}
// Download from NetworkStorage (WebDAV)
$downloaded = $this->downloadTemplateFromStorage($templatePath);
if ($downloaded) {
return $downloaded;
}
}
}
// Fallback: look in known locations
// Fallback: known locations
$fallbackPaths = [
'data/document-templates/direct-access-report.docx',
'custom/Espo/Modules/LegalAssistance/templates/direct-access-report.docx',
__DIR__ . '/../../../../../../templates/direct-access-report.docx',
];
foreach ($fallbackPaths as $path) {
@@ -267,8 +306,57 @@ class DirectAccessReportGenerator
throw new Error("Template '" . self::TEMPLATE_NAME . "' not found.");
}
private function downloadTemplateFromStorage(string $templatePath): ?string
{
try {
$nds = $this->injectableFactory->create(
\Espo\Modules\NetworkStorageIntegration\Services\NetworkDocumentService::class
);
$client = $nds->getClient();
// Try Templates/{templatePath} on the storage
$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)) {
$this->log->debug("LegalAssistance: Template not found on storage: {$storagePath}");
return null;
}
$content = $client->downloadFile($storagePath);
// Save locally for this request
if (!is_dir(self::TEMP_DIR)) {
mkdir(self::TEMP_DIR, 0775, true);
}
$localPath = self::TEMP_DIR . 'template_' . uniqid() . '.docx';
file_put_contents($localPath, $content);
$this->log->debug("LegalAssistance: Template downloaded from storage: {$storagePath}{$localPath}");
return $localPath;
} catch (\Exception $e) {
$this->log->warning("LegalAssistance: Failed to download template from storage: " . $e->getMessage());
return null;
}
}
private function processTemplate(string $inputPath, string $outputPath, array $data): void
{
// Ensure PHPWord autoloader is registered
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) {
+2 -2
View File
@@ -1,9 +1,9 @@
{
"name": "LegalAssistance",
"version": "2.0.0",
"version": "2.0.3",
"acceptableVersions": [">=8.0.0"],
"php": [">=8.1"],
"releaseDate": "2026-04-07",
"releaseDate": "2026-04-09",
"author": "Marcus-Law",
"description": "סיוע משפטי — יצירת דוח גישה ישירה (JSON ישירות ל-DOCX), אינטגרציה עם שירה",
"scripts": {
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -109,8 +109,8 @@
},
"recommendationType": {
"type": "string",
"enum": ["ייעוץ והדרכה חלף ייצוג", "ויתור הלקוח", "החלפת ייצוג", "לא נוצר קשר", "לא התקיימה פגישה", "עדיין לא ניתן להחליט", "נדרשים מסמכים נוספים", "המלצה לסירוב"],
"description": "סוג ההמלצה/ההודעה"
"enum": ["המשך ייצוג", "ייעוץ והדרכה חלף ייצוג", "ויתור הלקוח", "החלפת ייצוג", "לא נוצר קשר", "לא התקיימה פגישה", "עדיין לא ניתן להחליט", "נדרשים מסמכים נוספים", "המלצה לסירוב"],
"description": "סוג ההמלצה/ההודעה — בחר ׳המשך ייצוג׳ כשמומלץ להגיש ערעור או להמשיך בייצוג"
},
"recommendationDetail": {
"type": "string",
Binary file not shown.