Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7d901be29 | |||
| 3178904b28 | |||
| c1535e744f | |||
| 15602a976f | |||
| ffc60a6c0f | |||
| 53844ace9a | |||
| f8022279ad | |||
| 763638abd5 | |||
| 99bc442495 | |||
| 61fe16a0ea | |||
| 14979dba4b | |||
| 015586a9df | |||
| 3992384d96 | |||
| cf523cbd86 | |||
| 21cf570cbc | |||
| 4c0586cce8 | |||
| b3c219aa57 | |||
| 31e6359df4 | |||
| d4008569ca | |||
| 836594a863 | |||
| 19db7c1093 | |||
| ee55f1ecef | |||
| 0cca55da0c | |||
| ca3d5e7890 | |||
| be4de13618 | |||
| 682f13543e | |||
| c346642816 | |||
| 807fbcdde2 |
@@ -0,0 +1,12 @@
|
|||||||
|
# API Keys (Required to enable respective provider)
|
||||||
|
ANTHROPIC_API_KEY="your_anthropic_api_key_here" # Required: Format: sk-ant-api03-...
|
||||||
|
PERPLEXITY_API_KEY="your_perplexity_api_key_here" # Optional: Format: pplx-...
|
||||||
|
OPENAI_API_KEY="your_openai_api_key_here" # Optional, for OpenAI models. Format: sk-proj-...
|
||||||
|
GOOGLE_API_KEY="your_google_api_key_here" # Optional, for Google Gemini models.
|
||||||
|
MISTRAL_API_KEY="your_mistral_key_here" # Optional, for Mistral AI models.
|
||||||
|
XAI_API_KEY="YOUR_XAI_KEY_HERE" # Optional, for xAI AI models.
|
||||||
|
GROQ_API_KEY="YOUR_GROQ_KEY_HERE" # Optional, for Groq models.
|
||||||
|
OPENROUTER_API_KEY="YOUR_OPENROUTER_KEY_HERE" # Optional, for OpenRouter models.
|
||||||
|
AZURE_OPENAI_API_KEY="your_azure_key_here" # Optional, for Azure OpenAI models (requires endpoint in .taskmaster/config.json).
|
||||||
|
OLLAMA_API_KEY="your_ollama_api_key_here" # Optional: For remote Ollama servers that require authentication.
|
||||||
|
GITHUB_API_KEY="your_github_api_key_here" # Optional: For GitHub import/export features. Format: ghp_... or github_pat_...
|
||||||
+26
@@ -1,3 +1,29 @@
|
|||||||
*.bak
|
*.bak
|
||||||
*.tmp
|
*.tmp
|
||||||
/legal-docx-v3-fixed/
|
/legal-docx-v3-fixed/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
dev-debug.log
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
# OS specific
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
# tasks.json
|
||||||
|
# tasks/
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"main": {
|
||||||
|
"provider": "claude-code",
|
||||||
|
"modelId": "opus",
|
||||||
|
"maxTokens": 32000,
|
||||||
|
"temperature": 0.2
|
||||||
|
},
|
||||||
|
"research": {
|
||||||
|
"provider": "claude-code",
|
||||||
|
"modelId": "opus",
|
||||||
|
"maxTokens": 32000,
|
||||||
|
"temperature": 0.1
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"provider": "claude-code",
|
||||||
|
"modelId": "sonnet",
|
||||||
|
"maxTokens": 64000,
|
||||||
|
"temperature": 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"global": {
|
||||||
|
"logLevel": "info",
|
||||||
|
"debug": false,
|
||||||
|
"defaultNumTasks": 10,
|
||||||
|
"defaultSubtasks": 5,
|
||||||
|
"defaultPriority": "medium",
|
||||||
|
"projectName": "Task Master",
|
||||||
|
"ollamaBaseURL": "http://localhost:11434/api",
|
||||||
|
"bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com",
|
||||||
|
"responseLanguage": "Hebrew",
|
||||||
|
"enableCodebaseAnalysis": true,
|
||||||
|
"enableProxy": false,
|
||||||
|
"anonymousTelemetry": true,
|
||||||
|
"userId": "1234567890"
|
||||||
|
},
|
||||||
|
"claudeCode": {},
|
||||||
|
"codexCli": {},
|
||||||
|
"grokCli": {
|
||||||
|
"timeout": 120000,
|
||||||
|
"workingDirectory": null,
|
||||||
|
"defaultModel": "grok-4-latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"currentTag": "master",
|
||||||
|
"lastSwitched": "2026-06-03T07:25:36.666Z",
|
||||||
|
"branchTagMapping": {},
|
||||||
|
"migrationNoticeShown": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"master": {
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "fix: subject-only direct-access report naming (tight hyphen) + case-folder upload",
|
||||||
|
"description": "Report file uses subject-only canonical naming and is uploaded to the case folder correctly.",
|
||||||
|
"details": "DirectAccessReportGenerator: em-dash -> tight hyphen; sanitize via LocalFilesystemClient::sanitizeFileName; save Document with skipNetworkUpload, relate to Case, explicit uploadDocument + DB realign to canonical stored name.",
|
||||||
|
"testStrategy": "",
|
||||||
|
"status": "done",
|
||||||
|
"dependencies": [],
|
||||||
|
"priority": "high",
|
||||||
|
"subtasks": [],
|
||||||
|
"updatedAt": "2026-06-03T08:19:08.135Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "תיקון אבטחה: הסרת מחיקת טבלאות ועמודות מ-AfterUninstall",
|
||||||
|
"description": "הסרת הקריאות ל-dropTables ו-cleanCaseColumns מ-AfterUninstall.php כדי למנוע אובדן נתונים בלתי הפיך בעת הסרה/התקנה מחדש של ההרחבה.",
|
||||||
|
"details": "## רקע\n\nהממצא F-002 בביקורת האבטחה (SECURITY_AUDIT_2026-04-25.md:137-168) מזהה שהסקריפט `scripts/AfterUninstall.php` מבצע פעולות הרסניות:\n\n1. **מחיקת טבלאות** (שורות 54-73): `DROP TABLE legal_aid` ו-`DROP TABLE direct_access_report`\n2. **מחיקת עמודות** (שורות 75-101): `ALTER TABLE case DROP COLUMN` על 5 עמודות (`c_legal_aid_type`, `c_aid_type`, `c_urgency_level`, `c_appointment_date`, `c_filing_deadline`)\n\nזה מפר את **כלל N2** מ-EXTENSION_DEVELOPMENT_RULES.md:\n> כאשר הרחבה מוסיפה שדות לישויות ליבה (Case, Account, Contact וכו'), הסקריפט AfterUninstall חייב להסיר רק את ההפניות מ-layouts אבל **להשאיר את עמודות הדאטאבייס**, כך שנתוני הלקוח ישרדו מחזור הסרה/התקנה מחדש.\n\n## שינויים נדרשים\n\n### 1. הסרת הפונקציה `dropTables()` (שורות 54-73)\nמחק את כל הפונקציה והקריאה אליה בשורה 31.\n\n### 2. הסרת הפונקציה `cleanCaseColumns()` (שורות 75-101)\nמחק את כל הפונקציה והקריאה אליה בשורה 34.\n\n### 3. הוספת פונקציית ניקוי layouts (אופציונלי)\nאם יש layouts ב-`custom/Espo/Custom/Resources/layouts/Case/*.json` שמפנים לשדות שנוספו, יש להסיר את ההפניות:\n\n```php\nprivate function removeLayoutReferences(Log $log): void\n{\n $layoutDir = 'custom/Espo/Custom/Resources/layouts/Case';\n if (!is_dir($layoutDir)) {\n return;\n }\n\n $fieldsToRemove = [\n 'cLegalAidType',\n 'cAidType',\n 'cUrgencyLevel',\n 'cAppointmentDate',\n 'cFilingDeadline',\n ];\n\n foreach (glob($layoutDir . '/*.json') as $file) {\n $content = file_get_contents($file);\n $layout = json_decode($content, true);\n if (!$layout) continue;\n\n $modified = $this->removeFieldsFromLayout($layout, $fieldsToRemove);\n if ($modified) {\n file_put_contents($file, json_encode($layout, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));\n $log->info(\"LegalAssistance: Removed field references from {$file}\");\n }\n }\n}\n\nprivate function removeFieldsFromLayout(array &$layout, array $fields): bool\n{\n $modified = false;\n foreach ($layout as $key => &$value) {\n if (is_array($value)) {\n if (isset($value['name']) && in_array($value['name'], $fields)) {\n unset($layout[$key]);\n $modified = true;\n } else {\n $modified = $this->removeFieldsFromLayout($value, $fields) || $modified;\n }\n }\n }\n $layout = array_values($layout); // Re-index\n return $modified;\n}\n```\n\n### 4. עדכון תיעוד הקוד בראש הקובץ\nשנה את התיאור ב-PHPDoc להסביר שהסקריפט מסיר רק:\n- רשומות DocumentTemplate\n- קבצי i18n\n- הפניות ב-layouts (אם רלוונטי)\n- וניקוי cache\n\n**לא** מוחק טבלאות או עמודות.\n\n## מבנה הקובץ לאחר התיקון\n\n```php\n<?php\n/************************************************************************\n * LegalAssistance Extension — AfterUninstall\n *\n * 1. Remove DocumentTemplate records created by this extension\n * 2. Remove custom i18n files\n * 3. (Optional) Remove layout references to extension fields\n * 4. Clear cache\n *\n * Note: Database tables and columns are intentionally preserved to\n * protect customer data during uninstall/reinstall cycles (rule N2).\n ************************************************************************/\n\nclass AfterUninstall\n{\n public function run(Container $container): void\n {\n // ... (שמירה על removeDocumentTemplates, removeCustomI18n, clearCache)\n // הסרה מלאה של dropTables ו-cleanCaseColumns\n }\n}\n```",
|
||||||
|
"testStrategy": "## אסטרטגיית בדיקה\n\n### 1. בדיקת יחידה - סקריפט האנאינסטול\n- וודא שהקובץ `AfterUninstall.php` לא מכיל את המילים `DROP TABLE` או `DROP COLUMN`\n- וודא שאין קריאות לפונקציות `dropTables` או `cleanCaseColumns`\n\n### 2. בדיקת אינטגרציה בסביבת פיתוח\n1. **הכנה**: התקן את ההרחבה על instance ריק של EspoCRM\n2. **יצירת נתונים**: צור Case עם ערכים בשדות הרלוונטיים (אם קיימים)\n3. **הסרה**: הסר את ההרחבה דרך Administration → Extensions\n4. **אימות**: בדוק שהטבלאות `legal_aid` ו-`direct_access_report` עדיין קיימות (אם היו מלכתחילה)\n5. **אימות**: בדוק שהעמודות ב-Case עדיין קיימות (אם היו מלכתחילה)\n6. **התקנה מחדש**: התקן את ההרחבה שוב\n7. **אימות**: וודא שהנתונים שנשמרו בשלב 2 עדיין קיימים\n\n### 3. בדיקת לוגים\n- לאחר הסרה, בדוק את `data/logs/espo-YYYY-MM-DD.log`\n- וודא שאין שורות `Dropping table` או `Dropped column`\n- וודא שיש שורות `Removed DocumentTemplate` ו-`Cache cleared`\n\n### 4. בדיקת קוד סטטית\n```bash\n# בדוק שאין DROP statements\ngrep -n \"DROP TABLE\\|DROP COLUMN\" scripts/AfterUninstall.php\n# צפוי: אין תוצאות\n\n# בדוק שאין קריאות לפונקציות שהוסרו\ngrep -n \"dropTables\\|cleanCaseColumns\" scripts/AfterUninstall.php\n# צפוי: אין תוצאות\n```\n\n### 5. בדיקת רגרסיה\n- וודא שה-DocumentTemplate עדיין נמחק כראוי\n- וודא שקבצי i18n עדיין נמחקים כראוי\n- וודא ש-cache clearing עדיין עובד",
|
||||||
|
"status": "pending",
|
||||||
|
"dependencies": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"priority": "high",
|
||||||
|
"subtasks": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "תיקון אבטחה: הוספת בדיקת ACL ברמת רשומה ב-DirectAccessReportGenerator",
|
||||||
|
"description": "הוספת בדיקת הרשאות ברמת רשומה (record-level ACL) לכלי generate_direct_access_report למניעת IDOR — כרגע הכלי סומך רק על בדיקת scope-level ומאפשר למשתמש עם הרשאת קריאה לתיקים לצרף מסמכים לכל תיק במערכת.",
|
||||||
|
"details": "## רקע\n\nהממצא F-001 בביקורת האבטחה (SECURITY_AUDIT_2026-04-25.md:61-134) מזהה פגיעות IDOR חמורה:\n\nהכלי `DirectAccessReportGenerator.php` מקבל `caseId` שרירותי ומייצר מסמך DOCX שמצורף לתיק ללא בדיקת הרשאות ברמת הרשומה. הבדיקה היחידה שמתבצעת היא ב-upstream (SmartAssistant ActionExecutor) ברמת scope בלבד (`acl->checkScope('Case', 'read')`).\n\n**וקטור התקיפה:** כל משתמש EspoCRM מאומת עם הרשאת קריאה לתיקים יכול:\n1. לקרוא ל-`POST /api/v1/SmartAssistant/action/executeTool` עם `caseId` של תיק שאינו שלו\n2. הכלי קורא ל-`getEntityById('Case', $caseId)` שעוקף ACL כברירת מחדל\n3. מסמך DOCX מזויף נוצר ומצורף לתיק הקורבן\n4. המסמך מכיל תוכן שנשלט על ידי התוקף אך נושא את שם עורך הדין המוקצה לתיק\n\n## שינויים נדרשים\n\n### 1. הזרקת AclManager לקונסטרקטור\n\n```php\n// files/custom/Espo/Modules/LegalAssistance/SmartAssistant/Tools/DirectAccessReportGenerator.php\n\nuse Espo\\Core\\Acl;\nuse Espo\\Core\\Exceptions\\Forbidden;\n\n// ...\n\npublic function __construct(\n private EntityManager $entityManager,\n private InjectableFactory $injectableFactory,\n private Log $log,\n private User $user,\n private Acl $acl // הוסף\n) {}\n```\n\n### 2. בדיקת הרשאות ברמת רשומה בתחילת execute()\n\nבתחילת המתודה `execute()`, מיד אחרי שליפת התיק (שורה 41-44):\n\n```php\npublic function execute(array $params, ?string $caseId, string $userId): array\n{\n if (!$caseId) {\n throw new Error('generate_direct_access_report requires a case context.');\n }\n\n $this->log->info(\"LegalAssistance: Generating direct access report for case {$caseId}\");\n\n $case = $this->entityManager->getEntityById('Case', $caseId);\n if (!$case) {\n throw new Error(\"Case {$caseId} not found.\");\n }\n\n // === תיקון אבטחה F-001: בדיקת ACL ברמת רשומה ===\n // נדרשת הרשאת עריכה כי אנחנו מצרפים מסמך לתיק\n if (!$this->acl->checkEntityEdit($case)) {\n $this->log->warning(\n \"LegalAssistance: ACL denied — user {$this->user->getId()} \" .\n \"attempted to attach document to case {$caseId}\"\n );\n throw new Forbidden(\"אין לך הרשאה לצרף מסמכים לתיק זה.\");\n }\n\n // ... המשך הקוד הקיים\n}\n```\n\n### 3. שיקולי מימוש\n\n- **למה `checkEntityEdit` ולא `checkEntityRead`?** — יצירת Document וקישורו לתיק היא פעולת כתיבה, לא קריאה. משתמש עם read-only על התיק לא צריך לוכל לשנות את רשימת המסמכים שלו.\n\n- **למה `$this->acl` ולא `AclManager`?** — ה-`Acl` service כבר מוזרק עם ה-User הנכון (המשתמש המבצע את הבקשה). שימוש ב-`AclManager->checkEntity($user, $entity, 'edit')` היה דורש לשלוף את ה-User לפי `$userId` — מיותר כש-DI כבר מספק לנו את זה.\n\n- **לוגים:** חשוב לתעד ניסיונות גישה שנדחו לצורך אבחון וניטור אבטחה.\n\n- **שגיאה בעברית:** ההודעה מוחזרת למשתמש דרך shira-hermes, אז עברית מתאימה לחוויית משתמש.\n\n### 4. קובץ יחיד לעריכה\n\n`files/custom/Espo/Modules/LegalAssistance/SmartAssistant/Tools/DirectAccessReportGenerator.php`\n\n**שורות לעריכה:**\n- שורות 12-19: הוספת `use` statements\n- שורות 26-31: הוספת `Acl` לקונסטרקטור\n- שורות 40-44: הוספת בדיקת ACL אחרי שליפת התיק",
|
||||||
|
"testStrategy": "## אסטרטגיית בדיקה\n\n### 1. בדיקה ידנית — מניעת IDOR\n\n**הכנה:**\n1. צור שני משתמשים ב-EspoCRM:\n - `lawyer-a` — עם תיק משויך (נקרא לו `case-a`)\n - `lawyer-b` — ללא גישה ל-`case-a`\n\n2. וודא ש-`lawyer-b` יכול לקרוא תיקים (scope-level read) אבל לא את `case-a` (record-level)\n\n**בדיקה:**\n1. התחבר כ-`lawyer-b`\n2. קרא ל-API:\n ```bash\n curl -X POST https://espocrm.dev.marcus-law.co.il/api/v1/SmartAssistant/action/executeTool \\\n -H \"Content-Type: application/json\" \\\n -H \"X-Api-Key: <lawyer-b-api-key>\" \\\n -d '{\n \"tool\": \"generate_direct_access_report\",\n \"caseId\": \"<case-a-id>\",\n \"params\": {\"aidType\": \"יעוץ והדרכה\"}\n }'\n ```\n3. **תוצאה צפויה:** שגיאת 403 Forbidden עם הודעה \"אין לך הרשאה לצרף מסמכים לתיק זה.\"\n4. וודא שלא נוצר Document חדש ב-`case-a`\n\n### 2. בדיקת regression — פעולה תקינה\n\n1. התחבר כ-`lawyer-a` (בעל התיק)\n2. קרא לאותו API עם `caseId` של התיק שלו\n3. **תוצאה צפויה:** הדוח נוצר בהצלחה ומצורף לתיק\n\n### 3. בדיקת לוגים\n\n1. לאחר ניסיון IDOR שנדחה, בדוק בלוג:\n ```bash\n docker exec <espocrm-container> tail -20 data/logs/espo-$(date +%Y-%m-%d).log | grep \"ACL denied\"\n ```\n2. **תוצאה צפויה:** שורת לוג עם WARNING המציינת את user ID וה-case ID\n\n### 4. בדיקת אינטגרציה עם shira-hermes\n\n1. דרך ממשק Shira (צ'אט), בקש מעוזר המשפט ליצור דוח לתיק שאין לך גישה אליו\n2. **תוצאה צפויה:** הודעת שגיאה ידידותית בעברית מוחזרת למשתמש\n\n### 5. בדיקת unit (אופציונלי)\n\nאם קיימת תשתית PHPUnit בפרויקט:\n\n```php\npublic function testExecuteThrowsForbiddenWhenUserCannotEditCase(): void\n{\n // Mock Acl to return false for checkEntityEdit\n $acl = $this->createMock(Acl::class);\n $acl->method('checkEntityEdit')->willReturn(false);\n \n $generator = new DirectAccessReportGenerator(\n $this->entityManager,\n $this->injectableFactory,\n $this->log,\n $this->user,\n $acl\n );\n \n $this->expectException(Forbidden::class);\n $generator->execute(['aidType' => 'test'], 'some-case-id', 'user-id');\n}\n```",
|
||||||
|
"status": "pending",
|
||||||
|
"dependencies": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"priority": "high",
|
||||||
|
"subtasks": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"title": "feat: P7 — שיוך דוח גישה ישירה לתיקיית CaseFiles",
|
||||||
|
"description": "הוספת ענף CaseFiles ליצירת המסמכים: אם CaseFilesCore מותקן (soft-detect), המסמך שנוצר משויך לתיקיית 'מסמכים' של התיק (folderId) כדי שיופיע בפאנל CaseDocs. ההתנהגות הקיימת (קישור לתיק + העלאה ל-NetworkStorage אם פעיל) נשמרת — ללא רגרסיה.",
|
||||||
|
"details": "אומת על dev 21/06/2026 (P7_OK): php -l נקי; קישור דרך Case.documents נראה דרך Document.cases (אותה טבלת קישור), folderId נקבע לתיקיית 'מסמכים'. ענף זהה לנתב P5/P6 המאומת.",
|
||||||
|
"testStrategy": "dev: יצירת מסמך עם CaseFilesCore מותקן → folderId=מסמכים, נראה בפאנל.",
|
||||||
|
"status": "done",
|
||||||
|
"dependencies": [],
|
||||||
|
"priority": "medium",
|
||||||
|
"subtasks": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lastModified": "2026-06-03T08:19:08.136Z",
|
||||||
|
"taskCount": 1,
|
||||||
|
"completedCount": 1,
|
||||||
|
"tags": [
|
||||||
|
"master"
|
||||||
|
],
|
||||||
|
"created": "2026-06-20T12:07:53.783Z",
|
||||||
|
"description": "Tasks for master context",
|
||||||
|
"updated": "2026-06-21"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<context>
|
||||||
|
# Overview
|
||||||
|
[Provide a high-level overview of your product here. Explain what problem it solves, who it's for, and why it's valuable.]
|
||||||
|
|
||||||
|
# Core Features
|
||||||
|
[List and describe the main features of your product. For each feature, include:
|
||||||
|
- What it does
|
||||||
|
- Why it's important
|
||||||
|
- How it works at a high level]
|
||||||
|
|
||||||
|
# User Experience
|
||||||
|
[Describe the user journey and experience. Include:
|
||||||
|
- User personas
|
||||||
|
- Key user flows
|
||||||
|
- UI/UX considerations]
|
||||||
|
</context>
|
||||||
|
<PRD>
|
||||||
|
# Technical Architecture
|
||||||
|
[Outline the technical implementation details:
|
||||||
|
- System components
|
||||||
|
- Data models
|
||||||
|
- APIs and integrations
|
||||||
|
- Infrastructure requirements]
|
||||||
|
|
||||||
|
# Development Roadmap
|
||||||
|
[Break down the development process into phases:
|
||||||
|
- MVP requirements
|
||||||
|
- Future enhancements
|
||||||
|
- Do not think about timelines whatsoever -- all that matters is scope and detailing exactly what needs to be build in each phase so it can later be cut up into tasks]
|
||||||
|
|
||||||
|
# Logical Dependency Chain
|
||||||
|
[Define the logical order of development:
|
||||||
|
- Which features need to be built first (foundation)
|
||||||
|
- Getting as quickly as possible to something usable/visible front end that works
|
||||||
|
- Properly pacing and scoping each feature so it is atomic but can also be built upon and improved as development approaches]
|
||||||
|
|
||||||
|
# Risks and Mitigations
|
||||||
|
[Identify potential risks and how they'll be addressed:
|
||||||
|
- Technical challenges
|
||||||
|
- Figuring out the MVP that we can build upon
|
||||||
|
- Resource constraints]
|
||||||
|
|
||||||
|
# Appendix
|
||||||
|
[Include any additional information:
|
||||||
|
- Research findings
|
||||||
|
- Technical specifications]
|
||||||
|
</PRD>
|
||||||
@@ -0,0 +1,511 @@
|
|||||||
|
<rpg-method>
|
||||||
|
# Repository Planning Graph (RPG) Method - PRD Template
|
||||||
|
|
||||||
|
This template teaches you (AI or human) how to create structured, dependency-aware PRDs using the RPG methodology from Microsoft Research. The key insight: separate WHAT (functional) from HOW (structural), then connect them with explicit dependencies.
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
1. **Dual-Semantics**: Think functional (capabilities) AND structural (code organization) separately, then map them
|
||||||
|
2. **Explicit Dependencies**: Never assume - always state what depends on what
|
||||||
|
3. **Topological Order**: Build foundation first, then layers on top
|
||||||
|
4. **Progressive Refinement**: Start broad, refine iteratively
|
||||||
|
|
||||||
|
## How to Use This Template
|
||||||
|
|
||||||
|
- Follow the instructions in each `<instruction>` block
|
||||||
|
- Look at `<example>` blocks to see good vs bad patterns
|
||||||
|
- Fill in the content sections with your project details
|
||||||
|
- The AI reading this will learn the RPG method by following along
|
||||||
|
- Task Master will parse the resulting PRD into dependency-aware tasks
|
||||||
|
|
||||||
|
## Recommended Tools for Creating PRDs
|
||||||
|
|
||||||
|
When using this template to **create** a PRD (not parse it), use **code-context-aware AI assistants** for best results:
|
||||||
|
|
||||||
|
**Why?** The AI needs to understand your existing codebase to make good architectural decisions about modules, dependencies, and integration points.
|
||||||
|
|
||||||
|
**Recommended tools:**
|
||||||
|
- **Claude Code** (claude-code CLI) - Best for structured reasoning and large contexts
|
||||||
|
- **Cursor/Windsurf** - IDE integration with full codebase context
|
||||||
|
- **Gemini CLI** (gemini-cli) - Massive context window for large codebases
|
||||||
|
- **Codex/Grok CLI** - Strong code generation with context awareness
|
||||||
|
|
||||||
|
**Note:** Once your PRD is created, `task-master parse-prd` works with any configured AI model - it just needs to read the PRD text itself, not your codebase.
|
||||||
|
</rpg-method>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<overview>
|
||||||
|
<instruction>
|
||||||
|
Start with the problem, not the solution. Be specific about:
|
||||||
|
- What pain point exists?
|
||||||
|
- Who experiences it?
|
||||||
|
- Why existing solutions don't work?
|
||||||
|
- What success looks like (measurable outcomes)?
|
||||||
|
|
||||||
|
Keep this section focused - don't jump into implementation details yet.
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
[Describe the core problem. Be concrete about user pain points.]
|
||||||
|
|
||||||
|
## Target Users
|
||||||
|
[Define personas, their workflows, and what they're trying to achieve.]
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
[Quantifiable outcomes. Examples: "80% task completion via autopilot", "< 5% manual intervention rate"]
|
||||||
|
|
||||||
|
</overview>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<functional-decomposition>
|
||||||
|
<instruction>
|
||||||
|
Now think about CAPABILITIES (what the system DOES), not code structure yet.
|
||||||
|
|
||||||
|
Step 1: Identify high-level capability domains
|
||||||
|
- Think: "What major things does this system do?"
|
||||||
|
- Examples: Data Management, Core Processing, Presentation Layer
|
||||||
|
|
||||||
|
Step 2: For each capability, enumerate specific features
|
||||||
|
- Use explore-exploit strategy:
|
||||||
|
* Exploit: What features are REQUIRED for core value?
|
||||||
|
* Explore: What features make this domain COMPLETE?
|
||||||
|
|
||||||
|
Step 3: For each feature, define:
|
||||||
|
- Description: What it does in one sentence
|
||||||
|
- Inputs: What data/context it needs
|
||||||
|
- Outputs: What it produces/returns
|
||||||
|
- Behavior: Key logic or transformations
|
||||||
|
|
||||||
|
<example type="good">
|
||||||
|
Capability: Data Validation
|
||||||
|
Feature: Schema validation
|
||||||
|
- Description: Validate JSON payloads against defined schemas
|
||||||
|
- Inputs: JSON object, schema definition
|
||||||
|
- Outputs: Validation result (pass/fail) + error details
|
||||||
|
- Behavior: Iterate fields, check types, enforce constraints
|
||||||
|
|
||||||
|
Feature: Business rule validation
|
||||||
|
- Description: Apply domain-specific validation rules
|
||||||
|
- Inputs: Validated data object, rule set
|
||||||
|
- Outputs: Boolean + list of violated rules
|
||||||
|
- Behavior: Execute rules sequentially, short-circuit on failure
|
||||||
|
</example>
|
||||||
|
|
||||||
|
<example type="bad">
|
||||||
|
Capability: validation.js
|
||||||
|
(Problem: This is a FILE, not a CAPABILITY. Mixing structure into functional thinking.)
|
||||||
|
|
||||||
|
Capability: Validation
|
||||||
|
Feature: Make sure data is good
|
||||||
|
(Problem: Too vague. No inputs/outputs. Not actionable.)
|
||||||
|
</example>
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## Capability Tree
|
||||||
|
|
||||||
|
### Capability: [Name]
|
||||||
|
[Brief description of what this capability domain covers]
|
||||||
|
|
||||||
|
#### Feature: [Name]
|
||||||
|
- **Description**: [One sentence]
|
||||||
|
- **Inputs**: [What it needs]
|
||||||
|
- **Outputs**: [What it produces]
|
||||||
|
- **Behavior**: [Key logic]
|
||||||
|
|
||||||
|
#### Feature: [Name]
|
||||||
|
- **Description**:
|
||||||
|
- **Inputs**:
|
||||||
|
- **Outputs**:
|
||||||
|
- **Behavior**:
|
||||||
|
|
||||||
|
### Capability: [Name]
|
||||||
|
...
|
||||||
|
|
||||||
|
</functional-decomposition>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<structural-decomposition>
|
||||||
|
<instruction>
|
||||||
|
NOW think about code organization. Map capabilities to actual file/folder structure.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. Each capability maps to a module (folder or file)
|
||||||
|
2. Features within a capability map to functions/classes
|
||||||
|
3. Use clear module boundaries - each module has ONE responsibility
|
||||||
|
4. Define what each module exports (public interface)
|
||||||
|
|
||||||
|
The goal: Create a clear mapping between "what it does" (functional) and "where it lives" (structural).
|
||||||
|
|
||||||
|
<example type="good">
|
||||||
|
Capability: Data Validation
|
||||||
|
→ Maps to: src/validation/
|
||||||
|
├── schema-validator.js (Schema validation feature)
|
||||||
|
├── rule-validator.js (Business rule validation feature)
|
||||||
|
└── index.js (Public exports)
|
||||||
|
|
||||||
|
Exports:
|
||||||
|
- validateSchema(data, schema)
|
||||||
|
- validateRules(data, rules)
|
||||||
|
</example>
|
||||||
|
|
||||||
|
<example type="bad">
|
||||||
|
Capability: Data Validation
|
||||||
|
→ Maps to: src/utils.js
|
||||||
|
(Problem: "utils" is not a clear module boundary. Where do I find validation logic?)
|
||||||
|
|
||||||
|
Capability: Data Validation
|
||||||
|
→ Maps to: src/validation/everything.js
|
||||||
|
(Problem: One giant file. Features should map to separate files for maintainability.)
|
||||||
|
</example>
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## Repository Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
project-root/
|
||||||
|
├── src/
|
||||||
|
│ ├── [module-name]/ # Maps to: [Capability Name]
|
||||||
|
│ │ ├── [file].js # Maps to: [Feature Name]
|
||||||
|
│ │ └── index.js # Public exports
|
||||||
|
│ └── [module-name]/
|
||||||
|
├── tests/
|
||||||
|
└── docs/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Definitions
|
||||||
|
|
||||||
|
### Module: [Name]
|
||||||
|
- **Maps to capability**: [Capability from functional decomposition]
|
||||||
|
- **Responsibility**: [Single clear purpose]
|
||||||
|
- **File structure**:
|
||||||
|
```
|
||||||
|
module-name/
|
||||||
|
├── feature1.js
|
||||||
|
├── feature2.js
|
||||||
|
└── index.js
|
||||||
|
```
|
||||||
|
- **Exports**:
|
||||||
|
- `functionName()` - [what it does]
|
||||||
|
- `ClassName` - [what it does]
|
||||||
|
|
||||||
|
</structural-decomposition>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<dependency-graph>
|
||||||
|
<instruction>
|
||||||
|
This is THE CRITICAL SECTION for Task Master parsing.
|
||||||
|
|
||||||
|
Define explicit dependencies between modules. This creates the topological order for task execution.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. List modules in dependency order (foundation first)
|
||||||
|
2. For each module, state what it depends on
|
||||||
|
3. Foundation modules should have NO dependencies
|
||||||
|
4. Every non-foundation module should depend on at least one other module
|
||||||
|
5. Think: "What must EXIST before I can build this module?"
|
||||||
|
|
||||||
|
<example type="good">
|
||||||
|
Foundation Layer (no dependencies):
|
||||||
|
- error-handling: No dependencies
|
||||||
|
- config-manager: No dependencies
|
||||||
|
- base-types: No dependencies
|
||||||
|
|
||||||
|
Data Layer:
|
||||||
|
- schema-validator: Depends on [base-types, error-handling]
|
||||||
|
- data-ingestion: Depends on [schema-validator, config-manager]
|
||||||
|
|
||||||
|
Core Layer:
|
||||||
|
- algorithm-engine: Depends on [base-types, error-handling]
|
||||||
|
- pipeline-orchestrator: Depends on [algorithm-engine, data-ingestion]
|
||||||
|
</example>
|
||||||
|
|
||||||
|
<example type="bad">
|
||||||
|
- validation: Depends on API
|
||||||
|
- API: Depends on validation
|
||||||
|
(Problem: Circular dependency. This will cause build/runtime issues.)
|
||||||
|
|
||||||
|
- user-auth: Depends on everything
|
||||||
|
(Problem: Too many dependencies. Should be more focused.)
|
||||||
|
</example>
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## Dependency Chain
|
||||||
|
|
||||||
|
### Foundation Layer (Phase 0)
|
||||||
|
No dependencies - these are built first.
|
||||||
|
|
||||||
|
- **[Module Name]**: [What it provides]
|
||||||
|
- **[Module Name]**: [What it provides]
|
||||||
|
|
||||||
|
### [Layer Name] (Phase 1)
|
||||||
|
- **[Module Name]**: Depends on [[module-from-phase-0], [module-from-phase-0]]
|
||||||
|
- **[Module Name]**: Depends on [[module-from-phase-0]]
|
||||||
|
|
||||||
|
### [Layer Name] (Phase 2)
|
||||||
|
- **[Module Name]**: Depends on [[module-from-phase-1], [module-from-foundation]]
|
||||||
|
|
||||||
|
[Continue building up layers...]
|
||||||
|
|
||||||
|
</dependency-graph>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<implementation-roadmap>
|
||||||
|
<instruction>
|
||||||
|
Turn the dependency graph into concrete development phases.
|
||||||
|
|
||||||
|
Each phase should:
|
||||||
|
1. Have clear entry criteria (what must exist before starting)
|
||||||
|
2. Contain tasks that can be parallelized (no inter-dependencies within phase)
|
||||||
|
3. Have clear exit criteria (how do we know phase is complete?)
|
||||||
|
4. Build toward something USABLE (not just infrastructure)
|
||||||
|
|
||||||
|
Phase ordering follows topological sort of dependency graph.
|
||||||
|
|
||||||
|
<example type="good">
|
||||||
|
Phase 0: Foundation
|
||||||
|
Entry: Clean repository
|
||||||
|
Tasks:
|
||||||
|
- Implement error handling utilities
|
||||||
|
- Create base type definitions
|
||||||
|
- Setup configuration system
|
||||||
|
Exit: Other modules can import foundation without errors
|
||||||
|
|
||||||
|
Phase 1: Data Layer
|
||||||
|
Entry: Phase 0 complete
|
||||||
|
Tasks:
|
||||||
|
- Implement schema validator (uses: base types, error handling)
|
||||||
|
- Build data ingestion pipeline (uses: validator, config)
|
||||||
|
Exit: End-to-end data flow from input to validated output
|
||||||
|
</example>
|
||||||
|
|
||||||
|
<example type="bad">
|
||||||
|
Phase 1: Build Everything
|
||||||
|
Tasks:
|
||||||
|
- API
|
||||||
|
- Database
|
||||||
|
- UI
|
||||||
|
- Tests
|
||||||
|
(Problem: No clear focus. Too broad. Dependencies not considered.)
|
||||||
|
</example>
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## Development Phases
|
||||||
|
|
||||||
|
### Phase 0: [Foundation Name]
|
||||||
|
**Goal**: [What foundational capability this establishes]
|
||||||
|
|
||||||
|
**Entry Criteria**: [What must be true before starting]
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] [Task name] (depends on: [none or list])
|
||||||
|
- Acceptance criteria: [How we know it's done]
|
||||||
|
- Test strategy: [What tests prove it works]
|
||||||
|
|
||||||
|
- [ ] [Task name] (depends on: [none or list])
|
||||||
|
|
||||||
|
**Exit Criteria**: [Observable outcome that proves phase complete]
|
||||||
|
|
||||||
|
**Delivers**: [What can users/developers do after this phase?]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 1: [Layer Name]
|
||||||
|
**Goal**:
|
||||||
|
|
||||||
|
**Entry Criteria**: Phase 0 complete
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] [Task name] (depends on: [[tasks-from-phase-0]])
|
||||||
|
- [ ] [Task name] (depends on: [[tasks-from-phase-0]])
|
||||||
|
|
||||||
|
**Exit Criteria**:
|
||||||
|
|
||||||
|
**Delivers**:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[Continue with more phases...]
|
||||||
|
|
||||||
|
</implementation-roadmap>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<test-strategy>
|
||||||
|
<instruction>
|
||||||
|
Define how testing will be integrated throughout development (TDD approach).
|
||||||
|
|
||||||
|
Specify:
|
||||||
|
1. Test pyramid ratios (unit vs integration vs e2e)
|
||||||
|
2. Coverage requirements
|
||||||
|
3. Critical test scenarios
|
||||||
|
4. Test generation guidelines for Surgical Test Generator
|
||||||
|
|
||||||
|
This section guides the AI when generating tests during the RED phase of TDD.
|
||||||
|
|
||||||
|
<example type="good">
|
||||||
|
Critical Test Scenarios for Data Validation module:
|
||||||
|
- Happy path: Valid data passes all checks
|
||||||
|
- Edge cases: Empty strings, null values, boundary numbers
|
||||||
|
- Error cases: Invalid types, missing required fields
|
||||||
|
- Integration: Validator works with ingestion pipeline
|
||||||
|
</example>
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## Test Pyramid
|
||||||
|
|
||||||
|
```
|
||||||
|
/\
|
||||||
|
/E2E\ ← [X]% (End-to-end, slow, comprehensive)
|
||||||
|
/------\
|
||||||
|
/Integration\ ← [Y]% (Module interactions)
|
||||||
|
/------------\
|
||||||
|
/ Unit Tests \ ← [Z]% (Fast, isolated, deterministic)
|
||||||
|
/----------------\
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coverage Requirements
|
||||||
|
- Line coverage: [X]% minimum
|
||||||
|
- Branch coverage: [X]% minimum
|
||||||
|
- Function coverage: [X]% minimum
|
||||||
|
- Statement coverage: [X]% minimum
|
||||||
|
|
||||||
|
## Critical Test Scenarios
|
||||||
|
|
||||||
|
### [Module/Feature Name]
|
||||||
|
**Happy path**:
|
||||||
|
- [Scenario description]
|
||||||
|
- Expected: [What should happen]
|
||||||
|
|
||||||
|
**Edge cases**:
|
||||||
|
- [Scenario description]
|
||||||
|
- Expected: [What should happen]
|
||||||
|
|
||||||
|
**Error cases**:
|
||||||
|
- [Scenario description]
|
||||||
|
- Expected: [How system handles failure]
|
||||||
|
|
||||||
|
**Integration points**:
|
||||||
|
- [What interactions to test]
|
||||||
|
- Expected: [End-to-end behavior]
|
||||||
|
|
||||||
|
## Test Generation Guidelines
|
||||||
|
[Specific instructions for Surgical Test Generator about what to focus on, what patterns to follow, project-specific test conventions]
|
||||||
|
|
||||||
|
</test-strategy>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<architecture>
|
||||||
|
<instruction>
|
||||||
|
Describe technical architecture, data models, and key design decisions.
|
||||||
|
|
||||||
|
Keep this section AFTER functional/structural decomposition - implementation details come after understanding structure.
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## System Components
|
||||||
|
[Major architectural pieces and their responsibilities]
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
[Core data structures, schemas, database design]
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
[Languages, frameworks, key libraries]
|
||||||
|
|
||||||
|
**Decision: [Technology/Pattern]**
|
||||||
|
- **Rationale**: [Why chosen]
|
||||||
|
- **Trade-offs**: [What we're giving up]
|
||||||
|
- **Alternatives considered**: [What else we looked at]
|
||||||
|
|
||||||
|
</architecture>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<risks>
|
||||||
|
<instruction>
|
||||||
|
Identify risks that could derail development and how to mitigate them.
|
||||||
|
|
||||||
|
Categories:
|
||||||
|
- Technical risks (complexity, unknowns)
|
||||||
|
- Dependency risks (blocking issues)
|
||||||
|
- Scope risks (creep, underestimation)
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## Technical Risks
|
||||||
|
**Risk**: [Description]
|
||||||
|
- **Impact**: [High/Medium/Low - effect on project]
|
||||||
|
- **Likelihood**: [High/Medium/Low]
|
||||||
|
- **Mitigation**: [How to address]
|
||||||
|
- **Fallback**: [Plan B if mitigation fails]
|
||||||
|
|
||||||
|
## Dependency Risks
|
||||||
|
[External dependencies, blocking issues]
|
||||||
|
|
||||||
|
## Scope Risks
|
||||||
|
[Scope creep, underestimation, unclear requirements]
|
||||||
|
|
||||||
|
</risks>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<appendix>
|
||||||
|
## References
|
||||||
|
[Papers, documentation, similar systems]
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
[Domain-specific terms]
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
[Things to resolve during development]
|
||||||
|
</appendix>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<task-master-integration>
|
||||||
|
# How Task Master Uses This PRD
|
||||||
|
|
||||||
|
When you run `task-master parse-prd <file>.txt`, the parser:
|
||||||
|
|
||||||
|
1. **Extracts capabilities** → Main tasks
|
||||||
|
- Each `### Capability:` becomes a top-level task
|
||||||
|
|
||||||
|
2. **Extracts features** → Subtasks
|
||||||
|
- Each `#### Feature:` becomes a subtask under its capability
|
||||||
|
|
||||||
|
3. **Parses dependencies** → Task dependencies
|
||||||
|
- `Depends on: [X, Y]` sets task.dependencies = ["X", "Y"]
|
||||||
|
|
||||||
|
4. **Orders by phases** → Task priorities
|
||||||
|
- Phase 0 tasks = highest priority
|
||||||
|
- Phase N tasks = lower priority, properly sequenced
|
||||||
|
|
||||||
|
5. **Uses test strategy** → Test generation context
|
||||||
|
- Feeds test scenarios to Surgical Test Generator during implementation
|
||||||
|
|
||||||
|
**Result**: A dependency-aware task graph that can be executed in topological order.
|
||||||
|
|
||||||
|
## Why RPG Structure Matters
|
||||||
|
|
||||||
|
Traditional flat PRDs lead to:
|
||||||
|
- ❌ Unclear task dependencies
|
||||||
|
- ❌ Arbitrary task ordering
|
||||||
|
- ❌ Circular dependencies discovered late
|
||||||
|
- ❌ Poorly scoped tasks
|
||||||
|
|
||||||
|
RPG-structured PRDs provide:
|
||||||
|
- ✅ Explicit dependency chains
|
||||||
|
- ✅ Topological execution order
|
||||||
|
- ✅ Clear module boundaries
|
||||||
|
- ✅ Validated task graph before implementation
|
||||||
|
|
||||||
|
## Tips for Best Results
|
||||||
|
|
||||||
|
1. **Spend time on dependency graph** - This is the most valuable section for Task Master
|
||||||
|
2. **Keep features atomic** - Each feature should be independently testable
|
||||||
|
3. **Progressive refinement** - Start broad, use `task-master expand` to break down complex tasks
|
||||||
|
4. **Use research mode** - `task-master parse-prd --research` leverages AI for better task generation
|
||||||
|
</task-master-integration>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# LegalAssistance — Architecture (developer reference)
|
||||||
|
|
||||||
|
> Internal developer doc. Not shipped to clients. Customer overview: `README.md`. Security/dead-code: `_AUDIT/LegalAssistance/findings.md`.
|
||||||
|
|
||||||
|
**Module:** `LegalAssistance` · **Load order:** 25 · **Version:** 2.0.10 · **Author:** Marcus-Law · **Requires:** EspoCRM ≥ 8.0.0
|
||||||
|
|
||||||
|
## What it is
|
||||||
|
Generates the Israeli legal-aid "**דוח גישה ישירה**" (direct-access report) as a DOCX, straight from JSON params (no intermediate entity), and uploads it to the case folder. It is reached as a **SmartAssistant agent tool** — Shira runs an 8-step Hebrew interview to collect the data, then calls the tool. Since Phase 3, the legal tools live in **shira-hermes**, not ai-gateway; this extension is now minimal.
|
||||||
|
|
||||||
|
## File-by-file
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `files/.../SmartAssistant/Tools/DirectAccessReportGenerator.php` | **Core:** JSON params → DOCX via PHPWord `TemplateProcessor` (checkbox mapping) → upload to case folder via NetworkStorageIntegration (local fallback). **IDOR via caseId — S1; param→DOCX injection — S3; temp file — S4.** |
|
||||||
|
| `files/.../Resources/module.json` | Module order 25. |
|
||||||
|
| `files/.../Resources/metadata/app/smartAssistant.json` | Registers the `generate_direct_access_report` tool handler. |
|
||||||
|
| `files/.../Resources/metadata/entityDefs/Case.json` | **Empty `{}`** (no Case extension in v2.0.10) — D5. |
|
||||||
|
| `files/.../Resources/i18n/{en_US,he_IL,fa_IR}/Case.json` | Light "LegalAssistance" tab labels. (`Global.json` empty — D3.) |
|
||||||
|
| `plugins/legal-assistance/tools.json` | Shira tool schema for `generate_direct_access_report` (50+ params). |
|
||||||
|
| `plugins/legal-assistance/prompts.json` | Hebrew `casePromptAppend` — the 8-step interview flow. |
|
||||||
|
| `templates/direct-access-report.docx` | The DOCX template (39 KB binary). |
|
||||||
|
| `scripts/AfterInstall.php` | Creates the DocumentTemplate, clears cache, ensures i18n dirs. **ai-gateway deploy removed (Phase 3) — only a doc-comment remains.** |
|
||||||
|
| `scripts/AfterUninstall.php` | ⚠️ **Drops phantom v1.x tables + Case columns + i18n — destructive + dead (S2/D1).** |
|
||||||
|
|
||||||
|
### Dead/reference clutter (not packaged, but in the repo — D2/D4)
|
||||||
|
`docs/examples/entity-reference-legalaid.md` (59 KB, removed architecture), `field_mapping.md`, `legal-docx-v3-fixed/` (Node.js reference), `.env.example`, `טופס`, old zips v1.0.0–1.3.2.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- **NetworkStorageIntegration (strong, graceful):** report upload to the case folder; falls back to local attachment storage if disabled.
|
||||||
|
- **SmartAssistant (hard, runtime):** the tool is only reachable via SmartAssistant's `executeTool` — it inherits SmartAssistant's IDOR (S1).
|
||||||
|
- **shira-hermes (soft):** Shira loads the `plugins/` tool + prompt to drive the interview.
|
||||||
|
|
||||||
|
## Post-install gotchas
|
||||||
|
- Requires SmartAssistant + Shira configured; NetworkStorageIntegration recommended for case-folder filing.
|
||||||
|
- Fix the destructive `AfterUninstall` (S2) before any external release — it can wipe legal-aid data.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,39 @@
|
|||||||
|
# LegalAssistance — סיוע משפטי (דוח גישה ישירה)
|
||||||
|
|
||||||
|
**גרסה:** 2.0.10 | **מחבר:** Marcus-Law | **EspoCRM:** >= 8.0.0
|
||||||
|
|
||||||
|
## תיאור
|
||||||
|
|
||||||
|
יצירת **דוח גישה ישירה** (סיוע משפטי) כקובץ Word, ישירות מתוך שיחה עם העוזר **שירה**. שירה מנהל ראיון מובנה
|
||||||
|
(8 שלבים) לאיסוף הנתונים, ואז המערכת מייצרת את הדוח מתבנית DOCX וצורפת אותו לתיקיית התיק.
|
||||||
|
|
||||||
|
## תלויות
|
||||||
|
|
||||||
|
- **SmartAssistant + שירה (shira-hermes)** — נדרש; הכלי נקרא דרך מנגנון הכלים של שירה.
|
||||||
|
- **NetworkStorageIntegration** — מומלץ; שמירת הדוח בתיקיית התיק (יש fallback לאחסון מקומי).
|
||||||
|
|
||||||
|
## התקנה
|
||||||
|
|
||||||
|
1. ודא ש‑SmartAssistant ושירה מוגדרים.
|
||||||
|
2. הורד את `LegalAssistance` והתקן דרך **ניהול → הרחבות**.
|
||||||
|
3. בצע Rebuild ורענון קשיח.
|
||||||
|
|
||||||
|
## מה מתווסף למערכת
|
||||||
|
|
||||||
|
- כלי **"יצירת דוח גישה ישירה"** הזמין לעוזר שירה במצב תיק.
|
||||||
|
- תבנית מסמך דוח גישה ישירה.
|
||||||
|
|
||||||
|
## שימוש
|
||||||
|
|
||||||
|
במצב תיק, בקש משירה ליצור דוח גישה ישירה. שירה יאסוף את הפרטים בשלבים, ובסיום ייצור את ה‑DOCX וישמור אותו בתיק.
|
||||||
|
|
||||||
|
## טיפול בעיות נפוצות
|
||||||
|
|
||||||
|
| תסמין | סיבה | פתרון |
|
||||||
|
|---|---|---|
|
||||||
|
| הדוח לא נוצר | שירה/SmartAssistant לא מוגדרים | ודא שהאינטגרציה פעילה. |
|
||||||
|
| הדוח לא נשמר בתיק | NetworkStorageIntegration לא מותקן | יישמר מקומית; להתקנת NSI לשמירה בתיקיית התיק. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> תיעוד טכני למפתחים: `ARCHITECTURE.md`.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION=$(python3 -c "import json; print(json.load(open('manifest.json'))['version'])")
|
||||||
|
MODULE=$(python3 -c "import json; m=json.load(open('manifest.json')); print(m.get('module', m['name']))")
|
||||||
|
ZIPNAME="${MODULE}-${VERSION}.zip"
|
||||||
|
|
||||||
|
if [[ -f README.md ]]; then
|
||||||
|
sed -i "s/\*\*גרסה:\*\* [^ |]*/\*\*גרסה:\*\* ${VERSION}/" README.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building $ZIPNAME..."
|
||||||
|
rm -f "$ZIPNAME"
|
||||||
|
zip -r "$ZIPNAME" manifest.json files/ scripts/ \
|
||||||
|
-x "*.DS_Store" "*__MACOSX*" "*.zip" 2>/dev/null || \
|
||||||
|
zip -r "$ZIPNAME" manifest.json files/ \
|
||||||
|
-x "*.DS_Store" "*__MACOSX*" "*.zip"
|
||||||
|
|
||||||
|
echo "✓ Built: $ZIPNAME ($(du -h "$ZIPNAME" | cut -f1))"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,76 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Espo\Modules\LegalAssistance\Hooks\Case;
|
|
||||||
|
|
||||||
use Espo\ORM\Entity;
|
|
||||||
use Espo\ORM\EntityManager;
|
|
||||||
use Espo\Core\Utils\Log;
|
|
||||||
|
|
||||||
class CreateDirectAccessTasks
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private EntityManager $entityManager,
|
|
||||||
private Log $log
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function afterSave(Entity $entity, array $options): void
|
|
||||||
{
|
|
||||||
if (!empty($options['skipHooks'])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$entity->isAttributeChanged('cLegalAidType')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$newValue = $entity->get('cLegalAidType');
|
|
||||||
$oldValue = $entity->getFetched('cLegalAidType');
|
|
||||||
|
|
||||||
if ($newValue !== 'DirectAccess' || $oldValue === 'DirectAccess') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->log->info("LegalAssistance: Creating direct access tasks for case {$entity->getId()}");
|
|
||||||
|
|
||||||
$assignedUserId = $entity->get('assignedUserId');
|
|
||||||
$now = new \DateTime('now', new \DateTimeZone('Asia/Jerusalem'));
|
|
||||||
|
|
||||||
$tasks = [
|
|
||||||
[
|
|
||||||
'name' => 'שיחת טלפון ראשונה עם הלקוח',
|
|
||||||
'dateEnd' => (clone $now)->modify('+2 days')->format('Y-m-d'),
|
|
||||||
'priority' => 'High',
|
|
||||||
'description' => 'יש ליצור קשר ראשוני עם הלקוח תוך 48 שעות ממועד קבלת המינוי.',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'פגישה עם הלקוח',
|
|
||||||
'dateEnd' => (clone $now)->modify('+7 days')->format('Y-m-d'),
|
|
||||||
'priority' => 'Normal',
|
|
||||||
'description' => 'לקבוע ולקיים פגישה ראשונה עם הלקוח. לתעד נוכחים ותוכן הפגישה.',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'שליחת דוח גישה ישירה',
|
|
||||||
'dateEnd' => (clone $now)->modify('+14 days')->format('Y-m-d'),
|
|
||||||
'priority' => 'High',
|
|
||||||
'description' => 'למלא ולשלוח דוח דיווח ראשוני לסיוע המשפטי. ניתן לבקש משירה: "תיצרי דוח גישה ישירה".',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($tasks as $taskData) {
|
|
||||||
$task = $this->entityManager->getNewEntity('Task');
|
|
||||||
$task->set([
|
|
||||||
'name' => $taskData['name'],
|
|
||||||
'status' => 'Not Started',
|
|
||||||
'priority' => $taskData['priority'],
|
|
||||||
'dateEnd' => $taskData['dateEnd'],
|
|
||||||
'description' => $taskData['description'],
|
|
||||||
'parentType' => 'Case',
|
|
||||||
'parentId' => $entity->getId(),
|
|
||||||
'assignedUserId' => $assignedUserId,
|
|
||||||
]);
|
|
||||||
$this->entityManager->saveEntity($task, ['skipHooks' => false]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->log->info("LegalAssistance: Created 3 direct access tasks for case {$entity->getId()}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"LegalAssistance": "Legal Assistance"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"LegalAssistance": "Legal Assistance"
|
||||||
|
}
|
||||||
|
}
|
||||||
-80
@@ -1,80 +0,0 @@
|
|||||||
{
|
|
||||||
"scopeName": "Direct Access Report",
|
|
||||||
"scopeNamesPlural": "Direct Access Reports",
|
|
||||||
"labels": {
|
|
||||||
"Create DirectAccessReport": "Create Direct Access Report"
|
|
||||||
},
|
|
||||||
"fields": {
|
|
||||||
"name": "Report Name",
|
|
||||||
"number": "Number",
|
|
||||||
"status": "Status",
|
|
||||||
"case": "Case",
|
|
||||||
"aidType": "Aid Type",
|
|
||||||
"urgencyLevel": "Urgency Level",
|
|
||||||
"appointmentDate": "Appointment Date",
|
|
||||||
"filingDeadline": "Filing Deadline",
|
|
||||||
"additionalUrgency": "Additional Urgency",
|
|
||||||
"additionalDeadline": "Additional Deadline",
|
|
||||||
"firstContactDate": "First Contact Date",
|
|
||||||
"contactDelayReason": "Contact Delay Reason",
|
|
||||||
"meetingDate": "Meeting Date",
|
|
||||||
"meetingDelayReason": "Meeting Delay Reason",
|
|
||||||
"meetingAttendees": "Meeting Attendees",
|
|
||||||
"contactStatus": "Contact Status",
|
|
||||||
"contactStatusDetail": "Contact Status Detail",
|
|
||||||
"proceedingNumber": "Proceeding Number",
|
|
||||||
"courtName": "Court Name",
|
|
||||||
"proceedingSubject": "Proceeding Subject",
|
|
||||||
"claimSummary": "Claim Summary",
|
|
||||||
"defenseClaims": "Defense Claims",
|
|
||||||
"q1DocsReviewed": "Committee Reviewed All Documents?",
|
|
||||||
"q1DocsDetail": "Documents Detail",
|
|
||||||
"q2Reasoned": "Decision Reasoned?",
|
|
||||||
"q2ReasonDetail": "Missing Reasoning Detail",
|
|
||||||
"q3PanelMatch": "Panel Matches Impairments?",
|
|
||||||
"q4PanelComposition": "Panel Composition",
|
|
||||||
"q5ComplaintsAddressed": "All Complaints Addressed?",
|
|
||||||
"q6ComplaintsDetail": "Missing Complaints Detail",
|
|
||||||
"q7ClinicalExam": "Clinical Exam Performed?",
|
|
||||||
"q8ClinicalGap": "Clinical vs Documents Gap",
|
|
||||||
"q9MoharaApplied": "Mohara Doctrine Applied?",
|
|
||||||
"q9MoharaDetail": "Missing Mohara Data",
|
|
||||||
"q10RehabGap": "Rehab Officer vs Committee Gap?",
|
|
||||||
"q10RehabDetail": "Gap Detail",
|
|
||||||
"q11ScoreGap": "Score vs Condition Gap?",
|
|
||||||
"q11ScoreDetail": "Score Gap Detail",
|
|
||||||
"q12UniqueAspects": "Unique Aspects Addressed?",
|
|
||||||
"q12UniqueDetail": "Unique Aspects Detail",
|
|
||||||
"recommendationType": "Recommendation Type",
|
|
||||||
"recommendationDetail": "Recommendation Detail",
|
|
||||||
"additionalAidNeeded": "Additional Aid Needed",
|
|
||||||
"additionalAidNIDetail": "Additional NI Aid Detail",
|
|
||||||
"additionalAidOtherDetail": "Additional Other Aid Detail",
|
|
||||||
"additionalAidUrgency": "Additional Aid Urgency",
|
|
||||||
"additionalAidDeadlines": "Additional Aid Deadlines",
|
|
||||||
"willingToRepresent": "Willing to Represent?",
|
|
||||||
"notes": "General Notes",
|
|
||||||
"assignedUser": "Assigned User",
|
|
||||||
"teams": "Teams",
|
|
||||||
"createdAt": "Created At",
|
|
||||||
"modifiedAt": "Modified At",
|
|
||||||
"createdBy": "Created By",
|
|
||||||
"modifiedBy": "Modified By"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"status": {
|
|
||||||
"Draft": "Draft",
|
|
||||||
"Submitted": "Submitted",
|
|
||||||
"Approved": "Approved",
|
|
||||||
"Rejected": "Rejected"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"case": "Case"
|
|
||||||
},
|
|
||||||
"presetFilters": {
|
|
||||||
"draft": "Draft",
|
|
||||||
"submitted": "Submitted",
|
|
||||||
"approved": "Approved"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"LegalAssistance": "סיוע משפטי"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"LegalAssistance": "סיוע משפטי"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{
|
||||||
|
}
|
||||||
@@ -1,20 +1,8 @@
|
|||||||
{
|
{
|
||||||
"fields": {
|
"labels": {
|
||||||
"cLegalAidType": "סוג מינוי סיוע משפטי",
|
"LegalAssistance": "סיוע משפטי"
|
||||||
"cAppointmentDate": "מועד קבלת המינוי",
|
|
||||||
"cAidType": "מהות הסיוע",
|
|
||||||
"cUrgencyLevel": "דחיפות",
|
|
||||||
"cFilingDeadline": "מועד אחרון להגשה"
|
|
||||||
},
|
},
|
||||||
"options": {
|
"tabs": {
|
||||||
"cLegalAidType": {
|
"LegalAssistance": "סיוע משפטי"
|
||||||
"": "",
|
|
||||||
"DirectAccess": "גישה ישירה",
|
|
||||||
"Regular": "מינוי רגיל",
|
|
||||||
"Duty": "תורנות"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"directAccessReports": "דוחות גישה ישירה"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-80
@@ -1,80 +0,0 @@
|
|||||||
{
|
|
||||||
"scopeName": "דוח גישה ישירה",
|
|
||||||
"scopeNamesPlural": "דוחות גישה ישירה",
|
|
||||||
"labels": {
|
|
||||||
"Create DirectAccessReport": "צור דוח גישה ישירה"
|
|
||||||
},
|
|
||||||
"fields": {
|
|
||||||
"name": "שם הדוח",
|
|
||||||
"number": "מספר",
|
|
||||||
"status": "סטטוס",
|
|
||||||
"case": "תיק",
|
|
||||||
"aidType": "מהות הסיוע",
|
|
||||||
"urgencyLevel": "דחיפות הליך",
|
|
||||||
"appointmentDate": "מועד קבלת המינוי",
|
|
||||||
"filingDeadline": "מועד אחרון להגשה",
|
|
||||||
"additionalUrgency": "דחיפות הליך נוסף",
|
|
||||||
"additionalDeadline": "מועדים להליך נוסף",
|
|
||||||
"firstContactDate": "תאריך יצירת קשר ראשוני",
|
|
||||||
"contactDelayReason": "סיבת עיכוב (אם לא תוך 48 שעות)",
|
|
||||||
"meetingDate": "תאריך פגישה עם הלקוח",
|
|
||||||
"meetingDelayReason": "סיבת עיכוב פגישה",
|
|
||||||
"meetingAttendees": "נוכחים בפגישה",
|
|
||||||
"contactStatus": "מצב קשר עם הלקוח",
|
|
||||||
"contactStatusDetail": "פירוט מצב קשר",
|
|
||||||
"proceedingNumber": "מספר הליך",
|
|
||||||
"courtName": "בית הדין",
|
|
||||||
"proceedingSubject": "עניין ההליך",
|
|
||||||
"claimSummary": "תמצית התביעה/הערעור",
|
|
||||||
"defenseClaims": "טענות ההגנה",
|
|
||||||
"q1DocsReviewed": "הוועדה התייחסה לכל המסמכים?",
|
|
||||||
"q1DocsDetail": "פירוט מסמכים",
|
|
||||||
"q2Reasoned": "ההחלטה מנומקת?",
|
|
||||||
"q2ReasonDetail": "פירוט הנמקה חסרה",
|
|
||||||
"q3PanelMatch": "הרכב תואם ליקויים?",
|
|
||||||
"q4PanelComposition": "הרכב הוועדה / הרכב חסר",
|
|
||||||
"q5ComplaintsAddressed": "התייחסו לכל התלונות?",
|
|
||||||
"q6ComplaintsDetail": "פירוט תלונות חסרות",
|
|
||||||
"q7ClinicalExam": "בדיקה קלינית נערכה?",
|
|
||||||
"q8ClinicalGap": "פער בדיקה קלינית vs מסמכים",
|
|
||||||
"q9MoharaApplied": "בחינת הלכת מוהרה (גיל, רקע, השכלה)?",
|
|
||||||
"q9MoharaDetail": "נתונים חסרים (מוהרה)",
|
|
||||||
"q10RehabGap": "פער פקיד שיקום vs ועדה?",
|
|
||||||
"q10RehabDetail": "פירוט הפער",
|
|
||||||
"q11ScoreGap": "פער ניקוד vs מצב ומסמכים?",
|
|
||||||
"q11ScoreDetail": "פירוט פער הניקוד",
|
|
||||||
"q12UniqueAspects": "התייחסות להיבטים ייחודיים?",
|
|
||||||
"q12UniqueDetail": "פירוט היבטים ייחודיים",
|
|
||||||
"recommendationType": "סוג ההמלצה",
|
|
||||||
"recommendationDetail": "פירוט ההמלצה",
|
|
||||||
"additionalAidNeeded": "צורך בסיוע נוסף",
|
|
||||||
"additionalAidNIDetail": "פירוט סיוע נוסף (ביטוח לאומי)",
|
|
||||||
"additionalAidOtherDetail": "פירוט סיוע נוסף (תחום אחר)",
|
|
||||||
"additionalAidUrgency": "דחיפות הסיוע הנוסף",
|
|
||||||
"additionalAidDeadlines": "מועדים (סיוע נוסף)",
|
|
||||||
"willingToRepresent": "בתחום התמחות + מעוניין לייצג?",
|
|
||||||
"notes": "הערות כלליות",
|
|
||||||
"assignedUser": "אחראי",
|
|
||||||
"teams": "צוותים",
|
|
||||||
"createdAt": "נוצר ב",
|
|
||||||
"modifiedAt": "עודכן ב",
|
|
||||||
"createdBy": "נוצר על ידי",
|
|
||||||
"modifiedBy": "עודכן על ידי"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"status": {
|
|
||||||
"Draft": "טיוטה",
|
|
||||||
"Submitted": "נשלח",
|
|
||||||
"Approved": "אושר",
|
|
||||||
"Rejected": "נדחה"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"case": "תיק"
|
|
||||||
},
|
|
||||||
"presetFilters": {
|
|
||||||
"draft": "טיוטות",
|
|
||||||
"submitted": "נשלחו",
|
|
||||||
"approved": "אושרו"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{
|
||||||
|
}
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"tools": {
|
"tools": {
|
||||||
"generate_initial_report": {
|
"generate_direct_access_report": {
|
||||||
"handler": "Espo\\Modules\\LegalAssistance\\SmartAssistant\\Tools\\GenerateInitialReport",
|
"handler": "Espo\\Modules\\LegalAssistance\\SmartAssistant\\Tools\\DirectAccessReportGenerator",
|
||||||
"requiresCase": true
|
"requiresCase": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"controller": "controllers/record",
|
|
||||||
"boolFilterList": ["onlyMy"],
|
|
||||||
"iconClass": "fas fa-file-alt",
|
|
||||||
"color": "#6C3483",
|
|
||||||
"kanbanViewMode": true,
|
|
||||||
"statusField": "status",
|
|
||||||
"filterList": [
|
|
||||||
{
|
|
||||||
"name": "draft",
|
|
||||||
"style": "default"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "submitted",
|
|
||||||
"style": "warning"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "approved",
|
|
||||||
"style": "success"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"menu": {
|
|
||||||
"list": {
|
|
||||||
"buttons": []
|
|
||||||
},
|
|
||||||
"detail": {
|
|
||||||
"buttons": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"relationshipPanels": {
|
|
||||||
"case": {
|
|
||||||
"select": true,
|
|
||||||
"create": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +1,2 @@
|
|||||||
{
|
{
|
||||||
"fields": {
|
|
||||||
"cLegalAidType": {
|
|
||||||
"type": "enum",
|
|
||||||
"options": ["", "DirectAccess", "Regular", "Duty"],
|
|
||||||
"audited": true,
|
|
||||||
"displayAsLabel": true,
|
|
||||||
"style": {
|
|
||||||
"DirectAccess": "primary",
|
|
||||||
"Regular": "default",
|
|
||||||
"Duty": "info"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cAppointmentDate": {
|
|
||||||
"type": "date"
|
|
||||||
},
|
|
||||||
"cAidType": {
|
|
||||||
"type": "enum",
|
|
||||||
"options": ["", "הגשת ערעור", "יעוץ והדרכה", "סיוע נוסף ביטוח לאומי", "סיוע נוסף תחום א��ר", "אי מתן סיוע"]
|
|
||||||
},
|
|
||||||
"cUrgencyLevel": {
|
|
||||||
"type": "enum",
|
|
||||||
"options": ["", "רגיל", "דחוף", "��הול"],
|
|
||||||
"displayAsLabel": true,
|
|
||||||
"style": {
|
|
||||||
"רגיל": "default",
|
|
||||||
"דחוף": "warning",
|
|
||||||
"בהול": "danger"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cFilingDeadline": {
|
|
||||||
"type": "date"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"directAccessReports": {
|
|
||||||
"type": "hasMany",
|
|
||||||
"entity": "DirectAccessReport",
|
|
||||||
"foreign": "case",
|
|
||||||
"layoutRelationshipsDisabled": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
-269
@@ -1,269 +0,0 @@
|
|||||||
{
|
|
||||||
"fields": {
|
|
||||||
"name": {
|
|
||||||
"type": "varchar",
|
|
||||||
"required": true,
|
|
||||||
"pattern": "$noBadCharacters"
|
|
||||||
},
|
|
||||||
"number": {
|
|
||||||
"type": "autoincrement",
|
|
||||||
"index": true
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"type": "enum",
|
|
||||||
"options": ["Draft", "Submitted", "Approved", "Rejected"],
|
|
||||||
"default": "Draft",
|
|
||||||
"audited": true,
|
|
||||||
"displayAsLabel": true,
|
|
||||||
"style": {
|
|
||||||
"Draft": "default",
|
|
||||||
"Submitted": "warning",
|
|
||||||
"Approved": "success",
|
|
||||||
"Rejected": "danger"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"case": {
|
|
||||||
"type": "link",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"aidType": {
|
|
||||||
"type": "enum",
|
|
||||||
"options": ["", "הגשת ערעור", "יעוץ והדרכה", "סיוע נוסף ביטוח לאומי", "סיוע נוסף תחום אחר", "אי מתן סיוע"],
|
|
||||||
"audited": true
|
|
||||||
},
|
|
||||||
"urgencyLevel": {
|
|
||||||
"type": "enum",
|
|
||||||
"options": ["", "רגיל", "דחוף", "בהול"],
|
|
||||||
"default": "רגיל",
|
|
||||||
"displayAsLabel": true,
|
|
||||||
"style": {
|
|
||||||
"רגיל": "default",
|
|
||||||
"דחוף": "warning",
|
|
||||||
"בהול": "danger"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"appointmentDate": {
|
|
||||||
"type": "date"
|
|
||||||
},
|
|
||||||
"filingDeadline": {
|
|
||||||
"type": "date"
|
|
||||||
},
|
|
||||||
"additionalUrgency": {
|
|
||||||
"type": "enum",
|
|
||||||
"options": ["", "רגיל", "דחוף", "בהול"]
|
|
||||||
},
|
|
||||||
"additionalDeadline": {
|
|
||||||
"type": "varchar",
|
|
||||||
"maxLength": 150
|
|
||||||
},
|
|
||||||
|
|
||||||
"firstContactDate": {
|
|
||||||
"type": "date"
|
|
||||||
},
|
|
||||||
"contactDelayReason": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
"meetingDate": {
|
|
||||||
"type": "date"
|
|
||||||
},
|
|
||||||
"meetingDelayReason": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
"meetingAttendees": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
"contactStatus": {
|
|
||||||
"type": "enum",
|
|
||||||
"options": ["", "תקין", "לא תקין"]
|
|
||||||
},
|
|
||||||
"contactStatusDetail": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
|
|
||||||
"proceedingNumber": {
|
|
||||||
"type": "varchar",
|
|
||||||
"maxLength": 50
|
|
||||||
},
|
|
||||||
"courtName": {
|
|
||||||
"type": "enum",
|
|
||||||
"options": ["", "בית הדין האזורי לעבודה ירושלים", "בית הדין האזורי לעבודה תל אביב", "בית הדין האזורי לעבודה חיפה", "בית הדין האזורי לעבודה באר שבע", "בית הדין האזורי לעבודה נצרת", "בית הדין הארצי לעבודה"]
|
|
||||||
},
|
|
||||||
"proceedingSubject": {
|
|
||||||
"type": "enum",
|
|
||||||
"options": ["", "נכות כללית", "נכות מעבודה", "נכות איבה", "ניידות", "אי כושר", "שירותים מיוחדים", "סיעוד", "גמלת הבטחת הכנסה", "דמי אבטלה", "אחר"]
|
|
||||||
},
|
|
||||||
"claimSummary": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
"defenseClaims": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
|
|
||||||
"q1DocsReviewed": {
|
|
||||||
"type": "bool",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"q1DocsDetail": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
"q2Reasoned": {
|
|
||||||
"type": "bool",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"q2ReasonDetail": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
"q3PanelMatch": {
|
|
||||||
"type": "bool",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"q4PanelComposition": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
"q5ComplaintsAddressed": {
|
|
||||||
"type": "bool",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"q6ComplaintsDetail": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
"q7ClinicalExam": {
|
|
||||||
"type": "bool",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"q8ClinicalGap": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
|
|
||||||
"q9MoharaApplied": {
|
|
||||||
"type": "bool",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"q9MoharaDetail": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
"q10RehabGap": {
|
|
||||||
"type": "bool",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"q10RehabDetail": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
|
|
||||||
"q11ScoreGap": {
|
|
||||||
"type": "bool",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"q11ScoreDetail": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
"q12UniqueAspects": {
|
|
||||||
"type": "bool",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"q12UniqueDetail": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
|
|
||||||
"recommendationType": {
|
|
||||||
"type": "enum",
|
|
||||||
"options": ["", "ייעוץ והדרכה חלף ייצוג", "ויתור הלקוח", "החלפת ייצוג", "לא נוצר קשר", "לא התקיימה פגישה", "עדיין לא ניתן להחליט", "נדרשים מסמכים נוספים", "המלצה לסירוב"]
|
|
||||||
},
|
|
||||||
"recommendationDetail": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
|
|
||||||
"additionalAidNeeded": {
|
|
||||||
"type": "enum",
|
|
||||||
"options": ["", "לא נחוץ", "נחוץ בתחום ביטוח לאומי", "נחוץ בתחום אחר"]
|
|
||||||
},
|
|
||||||
"additionalAidNIDetail": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
"additionalAidOtherDetail": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
"additionalAidUrgency": {
|
|
||||||
"type": "enum",
|
|
||||||
"options": ["", "רגיל", "דחוף", "בהול"]
|
|
||||||
},
|
|
||||||
"additionalAidDeadlines": {
|
|
||||||
"type": "varchar",
|
|
||||||
"maxLength": 150
|
|
||||||
},
|
|
||||||
"willingToRepresent": {
|
|
||||||
"type": "bool",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"notes": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
|
|
||||||
"createdAt": {
|
|
||||||
"type": "datetime",
|
|
||||||
"readOnly": true
|
|
||||||
},
|
|
||||||
"modifiedAt": {
|
|
||||||
"type": "datetime",
|
|
||||||
"readOnly": true
|
|
||||||
},
|
|
||||||
"createdBy": {
|
|
||||||
"type": "link",
|
|
||||||
"readOnly": true,
|
|
||||||
"view": "views/fields/user"
|
|
||||||
},
|
|
||||||
"modifiedBy": {
|
|
||||||
"type": "link",
|
|
||||||
"readOnly": true,
|
|
||||||
"view": "views/fields/user"
|
|
||||||
},
|
|
||||||
"assignedUser": {
|
|
||||||
"type": "link",
|
|
||||||
"view": "views/fields/assigned-user"
|
|
||||||
},
|
|
||||||
"teams": {
|
|
||||||
"type": "linkMultiple",
|
|
||||||
"view": "views/fields/teams"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"case": {
|
|
||||||
"type": "belongsTo",
|
|
||||||
"entity": "Case",
|
|
||||||
"foreign": "directAccessReports"
|
|
||||||
},
|
|
||||||
"createdBy": {
|
|
||||||
"type": "belongsTo",
|
|
||||||
"entity": "User"
|
|
||||||
},
|
|
||||||
"modifiedBy": {
|
|
||||||
"type": "belongsTo",
|
|
||||||
"entity": "User"
|
|
||||||
},
|
|
||||||
"assignedUser": {
|
|
||||||
"type": "belongsTo",
|
|
||||||
"entity": "User"
|
|
||||||
},
|
|
||||||
"teams": {
|
|
||||||
"type": "hasMany",
|
|
||||||
"entity": "Team",
|
|
||||||
"relationName": "entityTeam",
|
|
||||||
"layoutRelationshipsDisabled": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"collection": {
|
|
||||||
"orderBy": "createdAt",
|
|
||||||
"order": "desc",
|
|
||||||
"textFilterFields": ["name", "number"]
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"status": {
|
|
||||||
"columns": ["status", "deleted"]
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"columns": ["createdAt"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-81
@@ -1,81 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"label": "כותרת ופרטי תיק",
|
|
||||||
"rows": [
|
|
||||||
[{"name": "name"}, {"name": "number"}],
|
|
||||||
[{"name": "case"}, {"name": "status"}],
|
|
||||||
[{"name": "assignedUser"}, {"name": "teams"}]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "מהות הסיוע",
|
|
||||||
"rows": [
|
|
||||||
[{"name": "aidType"}, {"name": "urgencyLevel"}],
|
|
||||||
[{"name": "appointmentDate"}, {"name": "filingDeadline"}],
|
|
||||||
[{"name": "additionalUrgency"}, {"name": "additionalDeadline"}]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "קשר עם הלקוח",
|
|
||||||
"rows": [
|
|
||||||
[{"name": "firstContactDate"}, {"name": "contactDelayReason"}],
|
|
||||||
[{"name": "meetingDate"}, {"name": "meetingDelayReason"}],
|
|
||||||
[{"name": "meetingAttendees"}, {"name": "contactStatus"}],
|
|
||||||
[{"name": "contactStatusDetail"}, false]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "פרטי ההליך",
|
|
||||||
"rows": [
|
|
||||||
[{"name": "proceedingNumber"}, {"name": "courtName"}],
|
|
||||||
[{"name": "proceedingSubject"}, false],
|
|
||||||
[{"name": "claimSummary", "fullWidth": true}],
|
|
||||||
[{"name": "defenseClaims", "fullWidth": true}]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "בחינת סיכוי משפטי — נכות",
|
|
||||||
"rows": [
|
|
||||||
[{"name": "q1DocsReviewed"}, {"name": "q1DocsDetail"}],
|
|
||||||
[{"name": "q2Reasoned"}, {"name": "q2ReasonDetail"}],
|
|
||||||
[{"name": "q3PanelMatch"}, {"name": "q4PanelComposition"}],
|
|
||||||
[{"name": "q5ComplaintsAddressed"}, {"name": "q6ComplaintsDetail"}],
|
|
||||||
[{"name": "q7ClinicalExam"}, {"name": "q8ClinicalGap"}]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "בחינת סיכוי משפטי — אי כושר",
|
|
||||||
"rows": [
|
|
||||||
[{"name": "q9MoharaApplied"}, {"name": "q9MoharaDetail"}],
|
|
||||||
[{"name": "q10RehabGap"}, {"name": "q10RehabDetail"}]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "בחינת סיכוי משפטי — שירותים מיוחדים/סיעוד",
|
|
||||||
"rows": [
|
|
||||||
[{"name": "q11ScoreGap"}, {"name": "q11ScoreDetail"}],
|
|
||||||
[{"name": "q12UniqueAspects"}, {"name": "q12UniqueDetail"}]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "המלצה",
|
|
||||||
"rows": [
|
|
||||||
[{"name": "recommendationType"}, false],
|
|
||||||
[{"name": "recommendationDetail", "fullWidth": true}]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "מיצוי זכויות",
|
|
||||||
"rows": [
|
|
||||||
[{"name": "additionalAidNeeded"}, {"name": "additionalAidUrgency"}],
|
|
||||||
[{"name": "additionalAidNIDetail"}, {"name": "additionalAidOtherDetail"}],
|
|
||||||
[{"name": "additionalAidDeadlines"}, {"name": "willingToRepresent"}]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "הערות",
|
|
||||||
"rows": [
|
|
||||||
[{"name": "notes", "fullWidth": true}]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"rows": [
|
|
||||||
[{"name": "name"}],
|
|
||||||
[{"name": "case"}],
|
|
||||||
[{"name": "status"}],
|
|
||||||
[{"name": "aidType"}],
|
|
||||||
[{"name": "recommendationType"}]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
[
|
|
||||||
{"name": "number", "width": 8},
|
|
||||||
{"name": "name", "link": true},
|
|
||||||
{"name": "case", "width": 20},
|
|
||||||
{"name": "status", "width": 12},
|
|
||||||
{"name": "aidType", "width": 15},
|
|
||||||
{"name": "recommendationType", "width": 15},
|
|
||||||
{"name": "createdAt", "width": 15},
|
|
||||||
{"name": "assignedUser", "width": 15}
|
|
||||||
]
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"entity": true,
|
|
||||||
"object": true,
|
|
||||||
"stream": true,
|
|
||||||
"tab": false,
|
|
||||||
"acl": true,
|
|
||||||
"aclPortal": false,
|
|
||||||
"disabled": false,
|
|
||||||
"module": "LegalAssistance",
|
|
||||||
"isCustom": false,
|
|
||||||
"notifications": true,
|
|
||||||
"calendar": false,
|
|
||||||
"activity": false,
|
|
||||||
"kanbanStatusIgnoreList": ["Approved", "Rejected"]
|
|
||||||
}
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Espo\Modules\LegalAssistance\Services;
|
|
||||||
|
|
||||||
use Espo\Core\Exceptions\Error;
|
|
||||||
use Espo\Core\InjectableFactory;
|
|
||||||
use Espo\ORM\EntityManager;
|
|
||||||
use Espo\Core\Utils\Log;
|
|
||||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
|
||||||
use Espo\Entities\User;
|
|
||||||
|
|
||||||
class DirectAccessReportService
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private EntityManager $entityManager,
|
|
||||||
private InjectableFactory $injectableFactory,
|
|
||||||
private Log $log,
|
|
||||||
private User $user,
|
|
||||||
private DateTimeUtil $dateTimeUtil
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function generateReport(string $caseId, string $userId, array $params): array
|
|
||||||
{
|
|
||||||
$case = $this->entityManager->getEntityById('Case', $caseId);
|
|
||||||
if (!$case) {
|
|
||||||
throw new Error("Case {$caseId} not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$contact = null;
|
|
||||||
$contactId = $case->get('contactId');
|
|
||||||
if ($contactId) {
|
|
||||||
$contact = $this->entityManager->getEntityById('Contact', $contactId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build report name
|
|
||||||
$contactName = $contact
|
|
||||||
? ($contact->get('firstName') . ' ' . $contact->get('lastName'))
|
|
||||||
: 'ללא איש קשר';
|
|
||||||
$reportName = "דוח גישה ישיר�� — {$contactName}";
|
|
||||||
|
|
||||||
// Extract legal analysis from nested object
|
|
||||||
$legalAnalysis = $params['legalAnalysis'] ?? [];
|
|
||||||
if (is_string($legalAnalysis)) {
|
|
||||||
$legalAnalysis = json_decode($legalAnalysis, true) ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create DirectAccessReport entity
|
|
||||||
$report = $this->entityManager->getNewEntity('DirectAccessReport');
|
|
||||||
$report->set([
|
|
||||||
'name' => $reportName,
|
|
||||||
'status' => 'Draft',
|
|
||||||
'caseId' => $caseId,
|
|
||||||
'assignedUserId' => $userId,
|
|
||||||
|
|
||||||
// Section ב' �� מהות הסיוע
|
|
||||||
'aidType' => $params['aidType'] ?? null,
|
|
||||||
'urgencyLevel' => $params['urgencyLevel'] ?? null,
|
|
||||||
'appointmentDate' => $this->normalizeDate($params['appointmentDate'] ?? null),
|
|
||||||
'filingDeadline' => $this->normalizeDate($params['filingDeadline'] ?? null),
|
|
||||||
'additionalUrgency' => $params['additionalUrgency'] ?? null,
|
|
||||||
'additionalDeadline' => $params['additionalDeadline'] ?? null,
|
|
||||||
|
|
||||||
// Section ג' — קשר עם הלקוח
|
|
||||||
'firstContactDate' => $this->normalizeDate($params['firstContactDate'] ?? null),
|
|
||||||
'contactDelayReason' => $params['contactDelayReason'] ?? null,
|
|
||||||
'meetingDate' => $this->normalizeDate($params['meetingDate'] ?? null),
|
|
||||||
'meetingDelayReason' => $params['meetingDelayReason'] ?? null,
|
|
||||||
'meetingAttendees' => $params['meetingAttendees'] ?? null,
|
|
||||||
'contactStatus' => $params['contactStatus'] ?? null,
|
|
||||||
'contactStatusDetail' => $params['contactStatusDetail'] ?? null,
|
|
||||||
|
|
||||||
// Section ד' — פרטי ההליך
|
|
||||||
'proceedingNumber' => $params['proceedingNumber'] ?? $case->get('cCourtCaseNumber'),
|
|
||||||
'courtName' => $params['courtName'] ?? null,
|
|
||||||
'proceedingSubject' => $params['proceedingSubject'] ?? null,
|
|
||||||
'claimSummary' => $params['claimSummary'] ?? null,
|
|
||||||
'defenseClaims' => $params['defenseClaims'] ?? null,
|
|
||||||
|
|
||||||
// Section ה'-ז' — בחינת סיכוי משפטי
|
|
||||||
'q1DocsReviewed' => $legalAnalysis['q1DocsReviewed'] ?? false,
|
|
||||||
'q1DocsDetail' => $legalAnalysis['q1DocsDetail'] ?? null,
|
|
||||||
'q2Reasoned' => $legalAnalysis['q2Reasoned'] ?? false,
|
|
||||||
'q2ReasonDetail' => $legalAnalysis['q2ReasonDetail'] ?? null,
|
|
||||||
'q3PanelMatch' => $legalAnalysis['q3PanelMatch'] ?? false,
|
|
||||||
'q4PanelComposition' => $legalAnalysis['q4PanelComposition'] ?? null,
|
|
||||||
'q5ComplaintsAddressed' => $legalAnalysis['q5ComplaintsAddressed'] ?? false,
|
|
||||||
'q6ComplaintsDetail' => $legalAnalysis['q6ComplaintsDetail'] ?? null,
|
|
||||||
'q7ClinicalExam' => $legalAnalysis['q7ClinicalExam'] ?? false,
|
|
||||||
'q8ClinicalGap' => $legalAnalysis['q8ClinicalGap'] ?? null,
|
|
||||||
'q9MoharaApplied' => $legalAnalysis['q9MoharaApplied'] ?? false,
|
|
||||||
'q9MoharaDetail' => $legalAnalysis['q9MoharaDetail'] ?? null,
|
|
||||||
'q10RehabGap' => $legalAnalysis['q10RehabGap'] ?? false,
|
|
||||||
'q10RehabDetail' => $legalAnalysis['q10RehabDetail'] ?? null,
|
|
||||||
'q11ScoreGap' => $legalAnalysis['q11ScoreGap'] ?? false,
|
|
||||||
'q11ScoreDetail' => $legalAnalysis['q11ScoreDetail'] ?? null,
|
|
||||||
'q12UniqueAspects' => $legalAnalysis['q12UniqueAspects'] ?? false,
|
|
||||||
'q12UniqueDetail' => $legalAnalysis['q12UniqueDetail'] ?? null,
|
|
||||||
|
|
||||||
// Section ח' — המלצה
|
|
||||||
'recommendationType' => $params['recommendationType'] ?? null,
|
|
||||||
'recommendationDetail' => $params['recommendationDetail'] ?? null,
|
|
||||||
|
|
||||||
// Section ט' — מיצוי זכויות
|
|
||||||
'additionalAidNeeded' => $params['additionalAidNeeded'] ?? null,
|
|
||||||
'additionalAidNIDetail' => $params['additionalAidNIDetail'] ?? null,
|
|
||||||
'additionalAidOtherDetail' => $params['additionalAidOtherDetail'] ?? null,
|
|
||||||
'additionalAidUrgency' => $params['additionalAidUrgency'] ?? null,
|
|
||||||
'additionalAidDeadlines' => $params['additionalAidDeadlines'] ?? null,
|
|
||||||
'willingToRepresent' => $params['willingToRepresent'] ?? false,
|
|
||||||
|
|
||||||
// Section י' — הערות
|
|
||||||
'notes' => $params['notes'] ?? null,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->entityManager->saveEntity($report);
|
|
||||||
|
|
||||||
$this->log->info("LegalAssistance: Created DirectAccessReport {$report->getId()} for case {$caseId}");
|
|
||||||
|
|
||||||
// Also update Case fields if provided
|
|
||||||
$caseUpdated = false;
|
|
||||||
if (!empty($params['appointmentDate']) && !$case->get('cAppointmentDate')) {
|
|
||||||
$case->set('cAppointmentDate', $this->normalizeDate($params['appointmentDate']));
|
|
||||||
$caseUpdated = true;
|
|
||||||
}
|
|
||||||
if (!empty($params['aidType']) && !$case->get('cAidType')) {
|
|
||||||
$case->set('cAidType', $params['aidType']);
|
|
||||||
$caseUpdated = true;
|
|
||||||
}
|
|
||||||
if (!empty($params['urgencyLevel']) && !$case->get('cUrgencyLevel')) {
|
|
||||||
$case->set('cUrgencyLevel', $params['urgencyLevel']);
|
|
||||||
$caseUpdated = true;
|
|
||||||
}
|
|
||||||
if (!empty($params['filingDeadline']) && !$case->get('cFilingDeadline')) {
|
|
||||||
$case->set('cFilingDeadline', $this->normalizeDate($params['filingDeadline']));
|
|
||||||
$caseUpdated = true;
|
|
||||||
}
|
|
||||||
if ($caseUpdated) {
|
|
||||||
$this->entityManager->saveEntity($case, ['skipHooks' => true]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate DOCX using template
|
|
||||||
$docxResult = $this->generateDocx($report, $case, $contact, $userId);
|
|
||||||
|
|
||||||
$msg = "✅ דוח גישה ישירה נוצר בהצלחה: \"{$reportName}\"";
|
|
||||||
if (!empty($docxResult['fileName'])) {
|
|
||||||
$msg .= "\n📄 מסמך DOCX נוצר: {$docxResult['fileName']}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => true,
|
|
||||||
'message' => $msg,
|
|
||||||
'entityType' => 'DirectAccessReport',
|
|
||||||
'entityId' => $report->getId(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function generateDocx($report, $case, $contact, string $userId): array
|
|
||||||
{
|
|
||||||
// Try to use LocalDocumentTemplateService if available
|
|
||||||
try {
|
|
||||||
$templateServiceClass = 'Espo\\Modules\\LocalDocuments\\Services\\LocalDocumentTemplateService';
|
|
||||||
if (!class_exists($templateServiceClass)) {
|
|
||||||
$templateServiceClass = 'Espo\\Modules\\NetworkStorageIntegration\\Services\\DocumentTemplateService';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!class_exists($templateServiceClass)) {
|
|
||||||
$this->log->warning("LegalAssistance: No template service available for DOCX generation");
|
|
||||||
return ['success' => false, 'error' => 'No template service'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the template
|
|
||||||
$template = $this->entityManager
|
|
||||||
->getRDBRepository('DocumentTemplate')
|
|
||||||
->where(['name' => 'דוח גישה ישירה'])
|
|
||||||
->findOne();
|
|
||||||
|
|
||||||
if (!$template) {
|
|
||||||
$this->log->warning("LegalAssistance: Template 'דוח גישה ישירה' not found");
|
|
||||||
return ['success' => false, 'error' => 'Template not found'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$templateService = $this->injectableFactory->create($templateServiceClass);
|
|
||||||
|
|
||||||
$result = $templateService->createFromTemplate(
|
|
||||||
$template->getId(),
|
|
||||||
'Case',
|
|
||||||
$case->getId(),
|
|
||||||
$report->get('name')
|
|
||||||
);
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$this->log->error("LegalAssistance: DOCX generation failed: " . $e->getMessage());
|
|
||||||
return ['success' => false, 'error' => $e->getMessage()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function normalizeDate(?string $value): ?string
|
|
||||||
{
|
|
||||||
if (!$value) return null;
|
|
||||||
|
|
||||||
// Handle DD/MM/YYYY format
|
|
||||||
if (preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/', $value, $m)) {
|
|
||||||
return "{$m[3]}-{$m[2]}-{$m[1]}";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle YYYY-MM-DD format (already correct)
|
|
||||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+475
@@ -0,0 +1,475 @@
|
|||||||
|
<?php
|
||||||
|
/************************************************************************
|
||||||
|
* Direct Access Report Generator
|
||||||
|
*
|
||||||
|
* Takes JSON params from Shira (AI assistant), generates a DOCX report
|
||||||
|
* using PHPWord TemplateProcessor, and attaches it to the Case as a
|
||||||
|
* Document. No entity creation — data goes directly into the template.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
namespace Espo\Modules\LegalAssistance\SmartAssistant\Tools;
|
||||||
|
|
||||||
|
use Espo\Core\Exceptions\Error;
|
||||||
|
use Espo\Core\InjectableFactory;
|
||||||
|
use Espo\ORM\EntityManager;
|
||||||
|
use Espo\Core\Utils\Log;
|
||||||
|
use Espo\Entities\User;
|
||||||
|
use Espo\Modules\NetworkStorageIntegration\Services\NetworkDocumentService;
|
||||||
|
use Espo\Modules\NetworkStorageIntegration\Classes\LocalFilesystemClient;
|
||||||
|
use PhpOffice\PhpWord\TemplateProcessor;
|
||||||
|
|
||||||
|
class DirectAccessReportGenerator
|
||||||
|
{
|
||||||
|
private const TEMP_DIR = 'data/tmp/';
|
||||||
|
private const TEMPLATE_NAME = 'דוח גישה ישירה';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private EntityManager $entityManager,
|
||||||
|
private InjectableFactory $injectableFactory,
|
||||||
|
private Log $log,
|
||||||
|
private User $user
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function execute(array $params, ?string $caseId, string $userId): array
|
||||||
|
{
|
||||||
|
if (!$caseId) {
|
||||||
|
throw new Error('generate_direct_access_report requires a case context.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log->info("LegalAssistance: Generating direct access report for case {$caseId}");
|
||||||
|
|
||||||
|
$case = $this->entityManager->getEntityById('Case', $caseId);
|
||||||
|
if (!$case) {
|
||||||
|
throw new Error("Case {$caseId} not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get contact
|
||||||
|
$contact = null;
|
||||||
|
$contactId = $case->get('contactId');
|
||||||
|
if (!$contactId) {
|
||||||
|
$contactsIds = $case->getLinkMultipleIdList('contacts');
|
||||||
|
if (!empty($contactsIds)) {
|
||||||
|
$contactId = $contactsIds[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($contactId) {
|
||||||
|
$contact = $this->entityManager->getEntityById('Contact', $contactId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$contactName = $contact
|
||||||
|
? trim($contact->get('firstName') . ' ' . $contact->get('lastName'))
|
||||||
|
: 'ללא איש קשר';
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Add checkbox mappings
|
||||||
|
$this->addCheckboxData($data, $params);
|
||||||
|
|
||||||
|
// Find template and generate DOCX. Subject-only, tight hyphen (rule N1).
|
||||||
|
$templatePath = $this->resolveTemplatePath();
|
||||||
|
$documentName = "דוח גישה ישירה-{$contactName}";
|
||||||
|
$baseName = LocalFilesystemClient::sanitizeFileName($documentName);
|
||||||
|
if ($baseName === '') {
|
||||||
|
$baseName = 'דוח גישה ישירה';
|
||||||
|
}
|
||||||
|
$fileName = $baseName . '.docx';
|
||||||
|
$outputPath = self::TEMP_DIR . uniqid('report_') . '.docx';
|
||||||
|
|
||||||
|
if (!is_dir(self::TEMP_DIR)) {
|
||||||
|
mkdir(self::TEMP_DIR, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->processTemplate($templatePath, $outputPath, $data);
|
||||||
|
|
||||||
|
// Read generated file
|
||||||
|
$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([
|
||||||
|
'name' => $fileName,
|
||||||
|
'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);
|
||||||
|
|
||||||
|
// Create Document linked to Case
|
||||||
|
$document = $this->entityManager->getNewEntity('Document');
|
||||||
|
$document->set([
|
||||||
|
'name' => $documentName,
|
||||||
|
'fileId' => $attachment->getId(),
|
||||||
|
'fileName' => $fileName,
|
||||||
|
'assignedUserId' => $userId,
|
||||||
|
'type' => 'דוח',
|
||||||
|
]);
|
||||||
|
// Suppress the NSI upload hook (fires before the Case link below); we
|
||||||
|
// upload explicitly with the Case so it lands in the case folder.
|
||||||
|
$this->entityManager->saveEntity($document, ['skipNetworkUpload' => true]);
|
||||||
|
|
||||||
|
// Link Document to Case (via Case side of relationship)
|
||||||
|
$this->entityManager->getRDBRepository('Case')
|
||||||
|
->getRelation($case, 'documents')
|
||||||
|
->relate($document);
|
||||||
|
|
||||||
|
// CaseFiles backend: place the Document in the case "מסמכים" folder so it
|
||||||
|
// appears in the CaseDocs panel tree (soft dependency, no hard requirement).
|
||||||
|
$caseFilesServiceClass = 'Espo\\Modules\\CaseFilesCore\\Services\\CaseFolderService';
|
||||||
|
if (class_exists($caseFilesServiceClass)) {
|
||||||
|
try {
|
||||||
|
$cfFolder = $this->injectableFactory->create($caseFilesServiceClass)
|
||||||
|
->getOrCreateSubfolder('Case', $caseId, 'מסמכים');
|
||||||
|
$document->set('folderId', $cfFolder->getId());
|
||||||
|
$this->entityManager->saveEntity($document, ['skipNetworkUpload' => true, 'silent' => true]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->log->warning('LegalAssistance: CaseFiles folder placement failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload to the case folder; align DB names to the canonical stored name.
|
||||||
|
try {
|
||||||
|
$nds = $this->injectableFactory->create(NetworkDocumentService::class);
|
||||||
|
if ($nds->isEnabled()) {
|
||||||
|
$result = $nds->uploadDocument($document, $attachment, 'Case', $caseId);
|
||||||
|
|
||||||
|
if (!empty($result['success'])) {
|
||||||
|
$storedName = $result['fileName'] ?? $fileName;
|
||||||
|
$storedBase = pathinfo($storedName, PATHINFO_FILENAME) ?: $documentName;
|
||||||
|
|
||||||
|
$document->set([
|
||||||
|
'name' => $storedBase,
|
||||||
|
'fileName' => $storedName,
|
||||||
|
'storageType' => 'network',
|
||||||
|
'networkStoragePath' => $result['path'] ?? null,
|
||||||
|
'networkStorageFileName' => $storedName,
|
||||||
|
'networkStorageSize' => $result['size'] ?? strlen($content),
|
||||||
|
'networkStorageModifiedAt' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
$this->entityManager->saveEntity($document, [
|
||||||
|
'skipNetworkUpload' => true,
|
||||||
|
'silent' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$attachment->set('name', $storedName);
|
||||||
|
$this->entityManager->saveEntity($attachment, ['silent' => true]);
|
||||||
|
|
||||||
|
$sourcePath = 'data/upload/' . $attachment->getSourceId();
|
||||||
|
if (file_exists($sourcePath)) {
|
||||||
|
@unlink($sourcePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
$documentName = $storedBase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Document is safely persisted in Espo; the network copy is best-effort.
|
||||||
|
$this->log->warning(
|
||||||
|
"LegalAssistance: failed to copy report to network storage for Case {$caseId}: " .
|
||||||
|
$e->getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log->info("LegalAssistance: Report generated — Document {$document->getId()} attached to Case {$caseId}");
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => "✅ דוח גישה ישירה נוצר בהצלחה: \"{$documentName}\"\n📄 המסמך צורף לתיק.",
|
||||||
|
'entityType' => 'Document',
|
||||||
|
'entityId' => $document->getId(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildPlaceholderData(array $params, $case, string $contactName, string $assignedUserName): array
|
||||||
|
{
|
||||||
|
$legalAnalysis = $params['legalAnalysis'] ?? [];
|
||||||
|
if (is_string($legalAnalysis)) {
|
||||||
|
$legalAnalysis = json_decode($legalAnalysis, true) ?? [];
|
||||||
|
} elseif (is_object($legalAnalysis)) {
|
||||||
|
$legalAnalysis = (array) $legalAnalysis;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
'cLegalAidNumber' => $params['legalAidNumber'] ?? $case->get('cLegalAidNumber') ?? '',
|
||||||
|
'contactName' => $contactName,
|
||||||
|
'assignedUserName' => $assignedUserName,
|
||||||
|
'reportDate' => $this->formatDate(date('Y-m-d')),
|
||||||
|
|
||||||
|
// Aid type & urgency
|
||||||
|
'appointmentDate' => $this->formatDate($this->normalizeDate($params['appointmentDate'] ?? null)),
|
||||||
|
'filingDeadline' => $this->formatDate($this->normalizeDate($params['filingDeadline'] ?? null)),
|
||||||
|
'additionalDeadline' => $params['additionalDeadline'] ?? '',
|
||||||
|
|
||||||
|
// Client contact
|
||||||
|
'firstContactDate' => $this->formatDate($this->normalizeDate($params['firstContactDate'] ?? null)),
|
||||||
|
'contactDelayReason' => $params['contactDelayReason'] ?? '',
|
||||||
|
'meetingDate' => $this->formatDate($this->normalizeDate($params['meetingDate'] ?? null)),
|
||||||
|
'meetingDelayReason' => $params['meetingDelayReason'] ?? '',
|
||||||
|
'meetingAttendees' => $params['meetingAttendees'] ?? '',
|
||||||
|
'contactStatusDetail' => $params['contactStatusDetail'] ?? '',
|
||||||
|
|
||||||
|
// Proceeding details
|
||||||
|
'proceedingNumber' => $params['proceedingNumber'] ?? $case->get('cCourtCaseNumber') ?? '',
|
||||||
|
'courtName' => $params['courtName'] ?? '',
|
||||||
|
'proceedingSubject' => $params['proceedingSubject'] ?? '',
|
||||||
|
'claimSummary' => $params['claimSummary'] ?? '',
|
||||||
|
'defenseClaims' => $params['defenseClaims'] ?? '',
|
||||||
|
|
||||||
|
// Legal analysis
|
||||||
|
'q1DocsDetail' => $legalAnalysis['q1DocsDetail'] ?? '',
|
||||||
|
'q2ReasonDetail' => $legalAnalysis['q2ReasonDetail'] ?? '',
|
||||||
|
'q4PanelComposition' => $legalAnalysis['q4PanelComposition'] ?? '',
|
||||||
|
'q6ComplaintsDetail' => $legalAnalysis['q6ComplaintsDetail'] ?? '',
|
||||||
|
'q8ClinicalGap' => $legalAnalysis['q8ClinicalGap'] ?? '',
|
||||||
|
'q9MoharaDetail' => $legalAnalysis['q9MoharaDetail'] ?? '',
|
||||||
|
'q10RehabDetail' => $legalAnalysis['q10RehabDetail'] ?? '',
|
||||||
|
'q11ScoreDetail' => $legalAnalysis['q11ScoreDetail'] ?? '',
|
||||||
|
'q12UniqueDetail' => $legalAnalysis['q12UniqueDetail'] ?? '',
|
||||||
|
|
||||||
|
// Recommendation
|
||||||
|
'recommendationDetail' => $params['recommendationDetail'] ?? '',
|
||||||
|
|
||||||
|
// Additional aid
|
||||||
|
'additionalAidNIDetail' => $params['additionalAidNIDetail'] ?? '',
|
||||||
|
'additionalAidOtherDetail' => $params['additionalAidOtherDetail'] ?? '',
|
||||||
|
'additionalAidDeadlines' => $params['additionalAidDeadlines'] ?? '',
|
||||||
|
|
||||||
|
// Notes
|
||||||
|
'notes' => $params['notes'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addCheckboxData(array &$data, array $params): void
|
||||||
|
{
|
||||||
|
$CHK = "☑\u{00A0}";
|
||||||
|
$UNCHK = "☐\u{00A0}";
|
||||||
|
|
||||||
|
$urgency = $params['urgencyLevel'] ?? '';
|
||||||
|
$data['chk_urgency_regular'] = ($urgency === 'רגיל') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_urgency_urgent'] = ($urgency === 'דחוף') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_urgency_emergency'] = ($urgency === 'בהול') ? $CHK : $UNCHK;
|
||||||
|
|
||||||
|
$aidType = $params['aidType'] ?? '';
|
||||||
|
$data['chk_aid_appeal'] = ($aidType === 'הגשת ערעור') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_aid_consulting'] = ($aidType === 'יעוץ והדרכה') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_aid_additional_ni'] = ($aidType === 'סיוע נוסף ביטוח לאומי') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_aid_additional_other'] = ($aidType === 'סיוע נוסף תחום אחר') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_aid_none'] = ($aidType === 'אי מתן סיוע') ? $CHK : $UNCHK;
|
||||||
|
|
||||||
|
$addUrgency = $params['additionalUrgency'] ?? '';
|
||||||
|
$data['chk_add_urgency_regular'] = ($addUrgency === 'רגיל') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_add_urgency_urgent'] = ($addUrgency === 'דחוף') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_add_urgency_emergency'] = ($addUrgency === 'בהול') ? $CHK : $UNCHK;
|
||||||
|
|
||||||
|
$contactStatus = $params['contactStatus'] ?? '';
|
||||||
|
$data['chk_contact_ok'] = ($contactStatus === 'תקין') ? $CHK : $UNCHK;
|
||||||
|
$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;
|
||||||
|
$data['chk_rec_no_contact'] = ($rec === 'לא נוצר קשר') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_rec_no_meeting'] = ($rec === 'לא התקיימה פגישה') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_rec_undecided'] = ($rec === 'עדיין לא ניתן להחליט') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_rec_docs_needed'] = ($rec === 'נדרשים מסמכים נוספים') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_rec_refusal'] = ($rec === 'המלצה לסירוב') ? $CHK : $UNCHK;
|
||||||
|
|
||||||
|
$addAid = $params['additionalAidNeeded'] ?? '';
|
||||||
|
$data['chk_no_additional'] = ($addAid === 'לא נחוץ') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_additional_ni'] = ($addAid === 'נחוץ בתחום ביטוח לאומי') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_additional_other'] = ($addAid === 'נחוץ בתחום אחר') ? $CHK : $UNCHK;
|
||||||
|
|
||||||
|
$addAidUrgency = $params['additionalAidUrgency'] ?? '';
|
||||||
|
$data['chk_add_aid_regular'] = ($addAidUrgency === 'רגיל') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_add_aid_urgent'] = ($addAidUrgency === 'דחוף') ? $CHK : $UNCHK;
|
||||||
|
$data['chk_add_aid_emergency'] = ($addAidUrgency === 'בהול') ? $CHK : $UNCHK;
|
||||||
|
|
||||||
|
$data['chk_willing_to_represent'] = !empty($params['willingToRepresent']) ? $CHK : $UNCHK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveTemplatePath(): string
|
||||||
|
{
|
||||||
|
$template = $this->entityManager
|
||||||
|
->getRDBRepository('DocumentTemplate')
|
||||||
|
->where(['name' => self::TEMPLATE_NAME])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($template) {
|
||||||
|
$templatePath = $template->get('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: known locations
|
||||||
|
$fallbackPaths = [
|
||||||
|
'data/document-templates/direct-access-report.docx',
|
||||||
|
'custom/Espo/Modules/LegalAssistance/templates/direct-access-report.docx',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($fallbackPaths as $path) {
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Fix space preservation: PHPWord strips whitespace from <w:t> elements
|
||||||
|
// that lack xml:space="preserve", causing words to stick together in RTL docs.
|
||||||
|
$zip = new \ZipArchive();
|
||||||
|
if ($zip->open($outputPath) === true) {
|
||||||
|
$xml = $zip->getFromName('word/document.xml');
|
||||||
|
$xml = preg_replace('/<w:t(?=[ >\/])(?![^>]*xml:space)/', '<w:t xml:space="preserve"', $xml);
|
||||||
|
$zip->addFromString('word/document.xml', $xml);
|
||||||
|
$zip->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeDate(?string $value): ?string
|
||||||
|
{
|
||||||
|
if (!$value) return null;
|
||||||
|
if (preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/', $value, $m)) {
|
||||||
|
return "{$m[3]}-{$m[2]}-{$m[1]}";
|
||||||
|
}
|
||||||
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatDate(?string $date): string
|
||||||
|
{
|
||||||
|
if (!$date) return '';
|
||||||
|
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $date, $m)) {
|
||||||
|
return "{$m[3]}.{$m[2]}.{$m[1]}";
|
||||||
|
}
|
||||||
|
return $date;
|
||||||
|
}
|
||||||
|
}
|
||||||
-33
@@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Espo\Modules\LegalAssistance\SmartAssistant\Tools;
|
|
||||||
|
|
||||||
use Espo\Core\Exceptions\Error;
|
|
||||||
use Espo\Core\InjectableFactory;
|
|
||||||
use Espo\ORM\EntityManager;
|
|
||||||
use Espo\Core\Utils\Log;
|
|
||||||
use Espo\Modules\LegalAssistance\Services\DirectAccessReportService;
|
|
||||||
|
|
||||||
class GenerateInitialReport
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private EntityManager $entityManager,
|
|
||||||
private InjectableFactory $injectableFactory,
|
|
||||||
private Log $log
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function execute(array $params, ?string $caseId, string $userId): array
|
|
||||||
{
|
|
||||||
if (!$caseId) {
|
|
||||||
throw new Error('generate_initial_report requires a case context.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->log->info("LegalAssistance: Generating initial report for case {$caseId}");
|
|
||||||
|
|
||||||
$service = $this->injectableFactory->create(DirectAccessReportService::class);
|
|
||||||
|
|
||||||
$result = $service->generateReport($caseId, $userId, $params);
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+9
-5
@@ -1,11 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "LegalAssistance",
|
"name": "LegalAssistance",
|
||||||
"version": "1.0.0",
|
"version": "2.1.0",
|
||||||
"acceptableVersions": [">=8.0.0"],
|
"acceptableVersions": [
|
||||||
"php": [">=8.1"],
|
">=8.0.0"
|
||||||
"releaseDate": "2026-04-06",
|
],
|
||||||
|
"php": [
|
||||||
|
">=8.1"
|
||||||
|
],
|
||||||
|
"releaseDate": "2026-06-21",
|
||||||
"author": "Marcus-Law",
|
"author": "Marcus-Law",
|
||||||
"description": "סיוע משפטי — דוח גישה ישירה, משימות אוטומטיות, אינטגרציה עם שירה",
|
"description": "סיוע משפטי — יצירת דוח גישה ישירה (JSON ישירות ל-DOCX), אינטגרציה עם שירה",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"afterInstall": "scripts/AfterInstall.php",
|
"afterInstall": "scripts/AfterInstall.php",
|
||||||
"afterUninstall": "scripts/AfterUninstall.php"
|
"afterUninstall": "scripts/AfterUninstall.php"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"tools": {
|
"tools": {
|
||||||
"generate_initial_report": {
|
"generate_direct_access_report": {
|
||||||
"description": "Generate Direct Access initial report (דוח דיווח ראשוני — גישה ישירה). Call ONLY after collecting ALL required data through conversation with the lawyer. This tool creates a DirectAccessReport entity and generates a DOCX document.",
|
"description": "Generate Direct Access report (דוח גישה ישירה) as DOCX and attach to Case. Call ONLY after collecting ALL required data through conversation with the lawyer. This tool generates the DOCX directly and attaches it to the Case as a Document — no intermediate entity is created.",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -67,6 +67,7 @@
|
|||||||
},
|
},
|
||||||
"courtName": {
|
"courtName": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"enum": ["בית הדין האזורי לעבודה ירושלים", "בית הדין האזורי לעבודה תל אביב", "בית הדין האזורי לעבודה חיפה", "בית הדין האזורי לעבודה באר שבע", "בית הדין האזורי לעבודה נצרת", "בית הדין הארצי לעבודה"],
|
||||||
"description": "בית הדין"
|
"description": "בית הדין"
|
||||||
},
|
},
|
||||||
"proceedingSubject": {
|
"proceedingSubject": {
|
||||||
@@ -108,8 +109,8 @@
|
|||||||
},
|
},
|
||||||
"recommendationType": {
|
"recommendationType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["ייעוץ והדרכה חלף ייצוג", "ויתור הלקוח", "החלפת ייצוג", "לא נוצר קשר", "לא התקיימה פגישה", "עדיין לא ניתן להחליט", "נדרשים מסמכים נוספים", "המלצה לסירוב"],
|
"enum": ["המשך ייצוג", "ייעוץ והדרכה חלף ייצוג", "ויתור הלקוח", "החלפת ייצוג", "לא נוצר קשר", "לא התקיימה פגישה", "עדיין לא ניתן להחליט", "נדרשים מסמכים נוספים", "המלצה לסירוב"],
|
||||||
"description": "סוג ההמלצה/ההודעה"
|
"description": "סוג ההמלצה/ההודעה — בחר ׳המשך ייצוג׳ כשמומלץ להגיש ערעור או להמשיך בייצוג"
|
||||||
},
|
},
|
||||||
"recommendationDetail": {
|
"recommendationDetail": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
+21
-43
@@ -2,9 +2,11 @@
|
|||||||
/************************************************************************
|
/************************************************************************
|
||||||
* LegalAssistance Extension — AfterInstall
|
* LegalAssistance Extension — AfterInstall
|
||||||
*
|
*
|
||||||
* 1. Deploy plugin files to ai-gateway plugins/ directory
|
* 1. Create DocumentTemplate for the direct access report
|
||||||
* 2. Create DocumentTemplate for the direct access report
|
* 2. Clear cache
|
||||||
* 3. Clear cache
|
*
|
||||||
|
* Note: ai-gateway plugin deployment removed — legal tools are now
|
||||||
|
* built into shira-hermes (api/services/legal_tools.py) since Phase 3.
|
||||||
************************************************************************/
|
************************************************************************/
|
||||||
|
|
||||||
use Espo\Core\Container;
|
use Espo\Core\Container;
|
||||||
@@ -24,13 +26,14 @@ class AfterInstall
|
|||||||
|
|
||||||
$log->info('LegalAssistance: Running AfterInstall...');
|
$log->info('LegalAssistance: Running AfterInstall...');
|
||||||
|
|
||||||
// 1. Deploy ai-gateway plugin files
|
// 0. Ensure custom i18n directories exist for all supported locales
|
||||||
$this->deployPluginFiles($log);
|
$config = $container->getByClass(Config::class);
|
||||||
|
$this->ensureI18nDirectories($config, $log);
|
||||||
|
|
||||||
// 2. Create DocumentTemplate
|
// 1. Create DocumentTemplate
|
||||||
$this->createDocumentTemplate($entityManager, $log);
|
$this->createDocumentTemplate($entityManager, $log);
|
||||||
|
|
||||||
// 3. Clear cache
|
// 2. Clear cache
|
||||||
try {
|
try {
|
||||||
$dataManager = $container->getByClass(DataManager::class);
|
$dataManager = $container->getByClass(DataManager::class);
|
||||||
$dataManager->clearCache();
|
$dataManager->clearCache();
|
||||||
@@ -42,50 +45,25 @@ class AfterInstall
|
|||||||
$log->info('LegalAssistance: AfterInstall completed.');
|
$log->info('LegalAssistance: AfterInstall completed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function deployPluginFiles(Log $log): void
|
private function ensureI18nDirectories(Config $config, Log $log): void
|
||||||
{
|
{
|
||||||
// Source: extension's plugins/ directory (relative to EspoCRM root after install)
|
$basePath = 'custom/Espo/Custom/Resources/i18n';
|
||||||
$sourceDir = 'custom/Espo/Modules/LegalAssistance/../../../../../../plugins/legal-assistance';
|
|
||||||
|
|
||||||
// Try common ai-gateway locations
|
$language = $config->get('language', 'en_US');
|
||||||
$gatewayPaths = [
|
|
||||||
'/home/chaim/ai-gateway/plugins/legal-assistance',
|
|
||||||
'/opt/ai-gateway/plugins/legal-assistance',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Find actual source files within the extension package
|
$locales = array_unique(['en_US', 'fa_IR', 'he_IL', $language]);
|
||||||
$extPluginDir = __DIR__ . '/../plugins/legal-assistance';
|
|
||||||
|
|
||||||
if (!is_dir($extPluginDir)) {
|
foreach ($locales as $locale) {
|
||||||
$log->warning("LegalAssistance: Plugin source directory not found at {$extPluginDir}");
|
$dir = $basePath . '/' . $locale;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($gatewayPaths as $targetDir) {
|
if (!is_dir($dir)) {
|
||||||
$parentDir = dirname($targetDir);
|
if (mkdir($dir, 0775, true)) {
|
||||||
if (!is_dir($parentDir)) {
|
$log->info("LegalAssistance: Created i18n directory: {$dir}");
|
||||||
continue;
|
} else {
|
||||||
}
|
$log->warning("LegalAssistance: Failed to create i18n directory: {$dir}");
|
||||||
|
|
||||||
if (!is_dir($targetDir)) {
|
|
||||||
mkdir($targetDir, 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$files = ['tools.json', 'prompts.json'];
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$src = $extPluginDir . '/' . $file;
|
|
||||||
$dst = $targetDir . '/' . $file;
|
|
||||||
if (file_exists($src)) {
|
|
||||||
copy($src, $dst);
|
|
||||||
$log->info("LegalAssistance: Deployed {$file} to {$targetDir}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$log->info("LegalAssistance: Plugin files deployed to {$targetDir}");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$log->warning('LegalAssistance: Could not find ai-gateway plugins directory. Deploy manually.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createDocumentTemplate(EntityManager $entityManager, Log $log): void
|
private function createDocumentTemplate(EntityManager $entityManager, Log $log): void
|
||||||
|
|||||||
+121
-20
@@ -2,12 +2,18 @@
|
|||||||
/************************************************************************
|
/************************************************************************
|
||||||
* LegalAssistance Extension — AfterUninstall
|
* LegalAssistance Extension — AfterUninstall
|
||||||
*
|
*
|
||||||
* 1. Remove plugin files from ai-gateway
|
* 1. Drop LegalAid and DirectAccessReport DB tables (with warning log)
|
||||||
* 2. Clear cache
|
* 2. Remove LegalAssistance-specific columns from Case table
|
||||||
|
* 3. Remove DocumentTemplate records created by this extension
|
||||||
|
* 4. Clear cache
|
||||||
|
*
|
||||||
|
* Note: ai-gateway plugin cleanup removed — legal tools are now
|
||||||
|
* built into shira-hermes since Phase 3.
|
||||||
************************************************************************/
|
************************************************************************/
|
||||||
|
|
||||||
use Espo\Core\Container;
|
use Espo\Core\Container;
|
||||||
use Espo\Core\DataManager;
|
use Espo\Core\DataManager;
|
||||||
|
use Espo\Core\ORM\EntityManager;
|
||||||
use Espo\Core\Utils\Log;
|
use Espo\Core\Utils\Log;
|
||||||
|
|
||||||
class AfterUninstall
|
class AfterUninstall
|
||||||
@@ -16,36 +22,131 @@ class AfterUninstall
|
|||||||
{
|
{
|
||||||
/** @var Log $log */
|
/** @var Log $log */
|
||||||
$log = $container->getByClass(Log::class);
|
$log = $container->getByClass(Log::class);
|
||||||
|
/** @var EntityManager $entityManager */
|
||||||
|
$entityManager = $container->getByClass(EntityManager::class);
|
||||||
|
|
||||||
$log->info('LegalAssistance: Running AfterUninstall...');
|
$log->info('LegalAssistance: Running AfterUninstall...');
|
||||||
|
|
||||||
// Remove ai-gateway plugin files
|
// 1. Drop tables created by this extension
|
||||||
$gatewayPaths = [
|
$this->dropTables($entityManager, $log);
|
||||||
'/home/chaim/ai-gateway/plugins/legal-assistance',
|
|
||||||
'/opt/ai-gateway/plugins/legal-assistance',
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($gatewayPaths as $dir) {
|
// 2. Remove LegalAssistance-specific columns from Case
|
||||||
if (is_dir($dir)) {
|
$this->cleanCaseColumns($entityManager, $log);
|
||||||
$files = glob($dir . '/*');
|
|
||||||
foreach ($files as $file) {
|
|
||||||
if (is_file($file)) {
|
|
||||||
unlink($file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rmdir($dir);
|
|
||||||
$log->info("LegalAssistance: Removed plugin files from {$dir}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear cache
|
// 3. Remove DocumentTemplate records
|
||||||
|
$this->removeDocumentTemplates($entityManager, $log);
|
||||||
|
|
||||||
|
// 4. Remove custom i18n files
|
||||||
|
$this->removeCustomI18n($log);
|
||||||
|
|
||||||
|
// 5. Clear cache
|
||||||
try {
|
try {
|
||||||
$dataManager = $container->getByClass(DataManager::class);
|
$dataManager = $container->getByClass(DataManager::class);
|
||||||
$dataManager->clearCache();
|
$dataManager->clearCache();
|
||||||
|
$log->info('LegalAssistance: Cache cleared.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$log->warning('LegalAssistance: Cache clear failed: ' . $e->getMessage());
|
$log->warning('LegalAssistance: Cache clear failed: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
$log->info('LegalAssistance: AfterUninstall completed.');
|
$log->info('LegalAssistance: AfterUninstall completed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function dropTables(EntityManager $entityManager, Log $log): void
|
||||||
|
{
|
||||||
|
$pdo = $entityManager->getPDO();
|
||||||
|
|
||||||
|
$tables = ['legal_aid', 'direct_access_report'];
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
try {
|
||||||
|
$check = $pdo->query("SHOW TABLES LIKE '{$table}'")->rowCount();
|
||||||
|
if ($check > 0) {
|
||||||
|
$count = $pdo->query("SELECT COUNT(*) FROM `{$table}`")->fetchColumn();
|
||||||
|
$log->warning("LegalAssistance: Dropping table '{$table}' ({$count} rows).");
|
||||||
|
$pdo->exec("DROP TABLE `{$table}`");
|
||||||
|
$log->info("LegalAssistance: Table '{$table}' dropped.");
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$log->warning("LegalAssistance: Failed to drop table '{$table}': " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cleanCaseColumns(EntityManager $entityManager, Log $log): void
|
||||||
|
{
|
||||||
|
$pdo = $entityManager->getPDO();
|
||||||
|
|
||||||
|
// Only columns that LegalAssistance module added to Case
|
||||||
|
$columns = [
|
||||||
|
'c_legal_aid_type',
|
||||||
|
'c_aid_type',
|
||||||
|
'c_urgency_level',
|
||||||
|
'c_appointment_date',
|
||||||
|
'c_filing_deadline',
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->query("SHOW COLUMNS FROM `case`");
|
||||||
|
$existing = array_column($stmt->fetchAll(\PDO::FETCH_ASSOC), 'Field');
|
||||||
|
|
||||||
|
foreach ($columns as $col) {
|
||||||
|
if (in_array($col, $existing)) {
|
||||||
|
$pdo->exec("ALTER TABLE `case` DROP COLUMN `{$col}`");
|
||||||
|
$log->info("LegalAssistance: Dropped column case.{$col}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$log->warning("LegalAssistance: Failed to clean Case columns: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeDocumentTemplates(EntityManager $entityManager, Log $log): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$template = $entityManager
|
||||||
|
->getRDBRepository('DocumentTemplate')
|
||||||
|
->where(['name' => 'דוח גישה ישירה'])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($template) {
|
||||||
|
$fileId = $template->get('fileId');
|
||||||
|
$entityManager->removeEntity($template);
|
||||||
|
$log->info("LegalAssistance: Removed DocumentTemplate 'דוח גישה ישירה'");
|
||||||
|
|
||||||
|
// Remove the attachment file
|
||||||
|
if ($fileId) {
|
||||||
|
$attachment = $entityManager->getEntityById('Attachment', $fileId);
|
||||||
|
if ($attachment) {
|
||||||
|
$filePath = 'data/upload/' . $fileId;
|
||||||
|
if (file_exists($filePath)) {
|
||||||
|
unlink($filePath);
|
||||||
|
}
|
||||||
|
$entityManager->removeEntity($attachment);
|
||||||
|
$log->info("LegalAssistance: Removed template attachment file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$log->warning("LegalAssistance: Failed to remove DocumentTemplate: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeCustomI18n(Log $log): void
|
||||||
|
{
|
||||||
|
$files = [
|
||||||
|
'custom/Espo/Custom/Resources/i18n/fa_IR/DirectAccessReport.json',
|
||||||
|
'custom/Espo/Custom/Resources/i18n/fa_IR/LegalAid.json',
|
||||||
|
'custom/Espo/Custom/Resources/i18n/he_IL/DirectAccessReport.json',
|
||||||
|
'custom/Espo/Custom/Resources/i18n/he_IL/LegalAid.json',
|
||||||
|
'custom/Espo/Custom/Resources/i18n/en_US/DirectAccessReport.json',
|
||||||
|
'custom/Espo/Custom/Resources/i18n/en_US/LegalAid.json',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (file_exists($file)) {
|
||||||
|
unlink($file);
|
||||||
|
$log->info("LegalAssistance: Removed {$file}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user