diff --git a/docs/examples/entity-reference-legalaid.md b/docs/examples/entity-reference-legalaid.md new file mode 100644 index 0000000..160e5db --- /dev/null +++ b/docs/examples/entity-reference-legalaid.md @@ -0,0 +1,1512 @@ +# EspoCRM Entity Reference: LegalAid + +A comprehensive, copy-paste-ready reference for creating new entities in an EspoCRM extension module. +Built from the real LegalAid entity in the LegalAssistance extension. + +--- + +## 1. Overview + +The **LegalAid** entity tracks legal-aid case reports inside EspoCRM. Each LegalAid record is linked to a **Case** and optionally to a **Contact**. It stores structured data about the legal proceeding, client meetings, legal-merit analysis (a series of boolean+text question pairs), recommendations, and additional-aid needs. + +**Why it was built:** The Israeli Legal Aid authority requires structured initial reports from appointed lawyers. This entity captures every section of that report as CRM fields, enabling AI-assisted report generation (via SmartAssistant tools), DOCX template export, and standard CRM workflows (stream, teams, assigned user, kanban). + +**Key design decisions:** +- Entity lives inside an extension module (`LegalAssistance`), not in `custom/Espo/Custom`. +- Uses `isCustom: true` in scopes so the Layout Manager in the admin UI can edit layouts. +- Hebrew enum options stored as-is in `entityDefs` (no translation indirection for domain-specific values). +- `fa_IR` locale is used as a wrapper for Hebrew (RTL support) -- its content is identical to `he_IL`. +- A custom client-side field view (`legal-aid-number.js`) turns a plain varchar on the Case entity into a clickable link that opens a filtered LegalAid list. + +--- + +## 2. Lessons Learned / Gotchas + +1. **Layouts go in `Resources/layouts/`, NOT `metadata/layouts/`.** + EspoCRM looks for module layouts under `Resources/layouts/{EntityName}/detail.json`, not under `metadata/layouts/`. Putting them under `metadata/` silently fails -- you just get the default auto-generated layout. + +2. **`isCustom: true` in scopes is required for Layout Manager.** + If you omit `"isCustom": true` from the scope definition, the admin Layout Manager will refuse to show the entity for layout editing. This is needed even for module-packaged entities. + +3. **You need an empty Controller class even for Record-based entities.** + Even if your entity uses 100% standard CRUD, you must create a `Controllers/YourEntity.php` that extends `Espo\Core\Controllers\Record`. Without it, API calls to `/api/v1/YourEntity` will 404. + +4. **`clientDefs` controller should be `"controllers/record"` (lowercase, no namespace).** + This refers to the client-side JS controller path, not the PHP namespace. Using the wrong value breaks the frontend routing. + +5. **Global.json must register the scope name in EVERY locale.** + The entity name appears in the navigation menu, tab list, and search. If `Global.json` does not include `scopeNames` and `scopeNamesPlural`, the entity shows its raw camelCase name. + +6. **`fa_IR` locale = Hebrew RTL wrapper.** + EspoCRM's RTL support uses `fa_IR` (Farsi) as the locale. For Hebrew UIs, duplicate `he_IL` content into `fa_IR`. Both must exist. + +7. **Link names in `entityDefs` must match on both sides.** + When defining `Case.links.legalAids` (hasMany) and `LegalAid.links.case` (belongsTo), the `foreign` property must reference the exact link name on the other entity. Mismatches cause silent failures. + +8. **Enum options with Hebrew values work fine.** + You can use Hebrew strings directly as enum option values. The i18n `options` block maps value-to-display-label, so even the Hebrew values get mapped (identity mapping in he_IL, translated in en_US). + +9. **`layoutRelationshipsDisabled: true` on links** prevents the link from appearing as a separate relationship panel. Use this when you handle the relationship display yourself (e.g., via a bottom panel or custom field view). + +10. **Custom field views need both a JS module and a Handlebars template.** + The JS class sets `detailTemplate` pointing to a `.tpl` file. The template path uses the module prefix (`legal-assistance:case/fields/legal-aid-number/detail`). The physical file lives under `res/templates/`. + +--- + +## 3. File Structure + +``` +LegalAssistance/ +├── files/ +│ ├── custom/Espo/Modules/LegalAssistance/ +│ │ ├── Controllers/ +│ │ │ └── LegalAid.php # Empty Record controller +│ │ ├── Resources/ +│ │ │ ├── metadata/ +│ │ │ │ ├── entityDefs/ +│ │ │ │ │ ├── LegalAid.json # Field/link/index definitions +│ │ │ │ │ └── Case.json # Extends Case with link + custom view +│ │ │ │ ├── scopes/ +│ │ │ │ │ └── LegalAid.json # Scope registration +│ │ │ │ └── clientDefs/ +│ │ │ │ └── LegalAid.json # Client-side config (icon, filters, kanban) +│ │ │ ├── layouts/ +│ │ │ │ └── LegalAid/ +│ │ │ │ ├── detail.json # Detail view layout +│ │ │ │ └── list.json # List view columns +│ │ │ └── i18n/ +│ │ │ ├── he_IL/ +│ │ │ │ ├── LegalAid.json # Hebrew translations +│ │ │ │ └── Global.json # Scope name registration (Hebrew) +│ │ │ ├── en_US/ +│ │ │ │ ├── LegalAid.json # English translations +│ │ │ │ └── Global.json # Scope name registration (English) +│ │ │ └── fa_IR/ +│ │ │ └── LegalAid.json # RTL wrapper (identical to he_IL) +│ │ └── SmartAssistant/ +│ │ └── Tools/ +│ │ └── GenerateInitialReport.php # AI tool: create/update LegalAid from params +│ └── client/custom/modules/legal-assistance/ +│ ├── src/views/case/fields/ +│ │ └── legal-aid-number.js # Custom field view (clickable link) +│ └── res/templates/case/fields/legal-aid-number/ +│ └── detail.tpl # Handlebars template for field view +``` + +--- + +## 4. File Contents + +### 4.1 Entity Metadata: `entityDefs/LegalAid.json` + +**Path:** `files/custom/Espo/Modules/LegalAssistance/Resources/metadata/entityDefs/LegalAid.json` + +```json +{ + "fields": { + "name": { + "type": "varchar", + "required": true, + "pattern": "$noBadCharacters" + }, + "number": { + "type": "autoincrement", + "index": true + }, + "status": { + "type": "enum", + "options": ["Draft", "Active", "Closed"], + "default": "Draft", + "audited": true, + "displayAsLabel": true, + "style": { + "Draft": "default", + "Active": "success", + "Closed": "info" + } + }, + "legalAidNumber": { + "type": "varchar", + "maxLength": 50 + }, + "legalAidType": { + "type": "enum", + "options": ["", "DirectAccess", "Regular", "Duty"], + "audited": true, + "displayAsLabel": true, + "style": { + "DirectAccess": "primary", + "Regular": "default", + "Duty": "info" + } + }, + "reportDate": { + "type": "date" + }, + "case": { + "type": "link", + "required": true + }, + "contact": { + "type": "link" + }, + "contactName": { + "type": "varchar", + "maxLength": 150 + }, + + "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": "legalAids" + }, + "contact": { + "type": "belongsTo", + "entity": "Contact" + }, + "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", "legalAidNumber"] + }, + "indexes": { + "status": { + "columns": ["status", "deleted"] + }, + "legalAidNumber": { + "columns": ["legalAidNumber", "deleted"] + }, + "createdAt": { + "columns": ["createdAt"] + } + } +} +``` + +### 4.2 Scope Registration: `scopes/LegalAid.json` + +**Path:** `files/custom/Espo/Modules/LegalAssistance/Resources/metadata/scopes/LegalAid.json` + +```json +{ + "entity": true, + "object": true, + "stream": true, + "tab": true, + "acl": true, + "aclPortal": false, + "disabled": false, + "module": "LegalAssistance", + "isCustom": true, + "layouts": true, + "notifications": true, + "calendar": false, + "activity": false, + "kanbanStatusIgnoreList": ["Closed"] +} +``` + +**Key properties explained:** +| Property | Value | Why | +|---|---|---| +| `entity` | `true` | This is a database-backed entity | +| `object` | `true` | Behaves as a standard CRM object (has detail/list views) | +| `stream` | `true` | Enable activity stream | +| `tab` | `true` | Show in the top navigation tab bar | +| `acl` | `true` | Enable role-based access control | +| `module` | `"LegalAssistance"` | Associates with the extension module | +| `isCustom` | `true` | **Critical:** allows Layout Manager editing in admin UI | +| `layouts` | `true` | Entity has custom layouts | +| `kanbanStatusIgnoreList` | `["Closed"]` | Hide "Closed" column from kanban board | + +### 4.3 Client Definitions: `clientDefs/LegalAid.json` + +**Path:** `files/custom/Espo/Modules/LegalAssistance/Resources/metadata/clientDefs/LegalAid.json` + +```json +{ + "controller": "controllers/record", + "modalDetailDisabled": false, + "quickCreateDisabled": true, + "boolFilterList": ["onlyMy"], + "iconClass": "fas fa-balance-scale", + "color": "#2E86C1", + "kanbanViewMode": true, + "statusField": "status", + "filterList": [ + { + "name": "draft", + "style": "default" + }, + { + "name": "active", + "style": "success" + }, + { + "name": "closed", + "style": "info" + } + ], + "menu": { + "list": { + "buttons": [] + }, + "detail": { + "buttons": [] + } + }, + "relationshipPanels": { + "case": { + "select": true, + "create": false + } + } +} +``` + +### 4.4 Controller: `Controllers/LegalAid.php` + +**Path:** `files/custom/Espo/Modules/LegalAssistance/Controllers/LegalAid.php` + +```php + **Note:** This file is required even though it is empty. Without it, the API endpoint `/api/v1/LegalAid` will return 404. EspoCRM's router resolves entity names to controller classes by convention. + +### 4.5 Detail Layout: `layouts/LegalAid/detail.json` + +**Path:** `files/custom/Espo/Modules/LegalAssistance/Resources/layouts/LegalAid/detail.json` + +```json +[ + { + "label": "כותרת ופרטי תיק", + "rows": [ + [{"name": "name"}, {"name": "number"}], + [{"name": "case"}, {"name": "status"}], + [{"name": "legalAidNumber"}, {"name": "legalAidType"}], + [{"name": "reportDate"}, false], + [{"name": "contact"}, {"name": "contactName"}], + [{"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}] + ] + } +] +``` + +**Layout conventions:** +- Each array element is a **panel** (collapsible section) with a `label` and `rows`. +- Each row is a 2-column array: `[leftField, rightField]`. Use `false` for an empty cell. +- `"fullWidth": true` makes a field span both columns (good for text areas). + +### 4.6 List Layout: `layouts/LegalAid/list.json` + +**Path:** `files/custom/Espo/Modules/LegalAssistance/Resources/layouts/LegalAid/list.json` + +```json +[ + {"name": "number", "width": 8}, + {"name": "name", "link": true}, + {"name": "legalAidNumber", "width": 12}, + {"name": "case", "width": 15}, + {"name": "contact", "width": 15}, + {"name": "status", "width": 10}, + {"name": "aidType", "width": 12}, + {"name": "urgencyLevel", "width": 8}, + {"name": "assignedUser", "width": 12} +] +``` + +**List layout conventions:** +- `"link": true` makes the field value a clickable link to the record detail view. +- `"width"` is a percentage. Widths should roughly sum to ~100. + +### 4.7 i18n: Hebrew (`he_IL/LegalAid.json`) + +**Path:** `files/custom/Espo/Modules/LegalAssistance/Resources/i18n/he_IL/LegalAid.json` + +```json +{ + "scopeName": "סיוע משפטי", + "scopeNamesPlural": "סיוע משפטי", + "labels": { + "Create LegalAid": "צור רשומת סיוע משפטי" + }, + "fields": { + "name": "שם", + "number": "מספר", + "status": "סטטוס", + "legalAidNumber": "מספר תיק סיוע משפטי", + "legalAidType": "סוג מינוי", + "reportDate": "תאריך הדוח", + "case": "תיק", + "contact": "איש קשר", + "contactName": "שם הלקוח", + "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": "טיוטה", + "Active": "פעיל", + "Closed": "סגור" + }, + "legalAidType": { + "": "", + "DirectAccess": "גישה ישירה", + "Regular": "מינוי רגיל", + "Duty": "תורנות" + }, + "aidType": { + "": "", + "הגשת ערעור": "הגשת ערעור", + "יעוץ והדרכה": "יעוץ והדרכה", + "סיוע נוסף ביטוח לאומי": "סיוע נוסף ביטוח לאומי", + "סיוע נוסף תחום אחר": "סיוע נוסף תחום אחר", + "אי מתן סיוע": "אי מתן סיוע" + }, + "urgencyLevel": { + "": "", + "רגיל": "רגיל", + "דחוף": "דחוף", + "בהול": "בהול" + }, + "additionalUrgency": { + "": "", + "רגיל": "רגיל", + "דחוף": "דחוף", + "בהול": "בהול" + }, + "contactStatus": { + "": "", + "תקין": "תקין", + "לא תקין": "לא תקין" + }, + "courtName": { + "": "", + "בית הדין האזורי לעבודה ירושלים": "בית הדין האזורי לעבודה ירושלים", + "בית הדין האזורי לעבודה תל אביב": "בית הדין האזורי לעבודה תל אביב", + "בית הדין האזורי לעבודה חיפה": "בית הדין האזורי לעבודה חיפה", + "בית הדין האזורי לעבודה באר שבע": "בית הדין האזורי לעבודה באר שבע", + "בית הדין האזורי לעבודה נצרת": "בית הדין האזורי לעבודה נצרת", + "בית הדין הארצי לעבודה": "בית הדין הארצי לעבודה" + }, + "proceedingSubject": { + "": "", + "נכות כללית": "נכות כללית", + "נכות מעבודה": "נכות מעבודה", + "נכות איבה": "נכות איבה", + "ניידות": "ניידות", + "אי כושר": "אי כושר", + "שירותים מיוחדים": "שירותים מיוחדים", + "סיעוד": "סיעוד", + "גמלת הבטחת הכנסה": "גמלת הבטחת הכנסה", + "דמי אבטלה": "דמי אבטלה", + "אחר": "אחר" + }, + "recommendationType": { + "": "", + "ייעוץ והדרכה חלף ייצוג": "ייעוץ והדרכה חלף ייצוג", + "ויתור הלקוח": "ויתור הלקוח", + "החלפת ייצוג": "החלפת ייצוג", + "לא נוצר קשר": "לא נוצר קשר", + "לא התקיימה פגישה": "לא התקיימה פגישה", + "עדיין לא ניתן להחליט": "עדיין לא ניתן להחליט", + "נדרשים מסמכים נוספים": "נדרשים מסמכים נוספים", + "המלצה לסירוב": "המלצה לסירוב" + }, + "additionalAidNeeded": { + "": "", + "לא נחוץ": "לא נחוץ", + "נחוץ בתחום ביטוח לאומי": "נחוץ בתחום ביטוח לאומי", + "נחוץ בתחום אחר": "נחוץ בתחום אחר" + }, + "additionalAidUrgency": { + "": "", + "רגיל": "רגיל", + "דחוף": "דחוף", + "בהול": "בהול" + } + }, + "links": { + "case": "תיק", + "contact": "איש קשר" + }, + "presetFilters": { + "draft": "טיוטות", + "active": "פעילים", + "closed": "סגורים" + } +} +``` + +### 4.8 i18n: English (`en_US/LegalAid.json`) + +**Path:** `files/custom/Espo/Modules/LegalAssistance/Resources/i18n/en_US/LegalAid.json` + +```json +{ + "scopeName": "Legal Aid", + "scopeNamesPlural": "Legal Aid Records", + "labels": { + "Create LegalAid": "Create Legal Aid Record" + }, + "fields": { + "name": "Name", + "number": "Number", + "status": "Status", + "legalAidNumber": "Legal Aid Number", + "legalAidType": "Legal Aid Type", + "reportDate": "Report Date", + "case": "Case", + "contact": "Contact", + "contactName": "Client Name", + "aidType": "Aid Type", + "urgencyLevel": "Urgency Level", + "appointmentDate": "Appointment Date", + "filingDeadline": "Filing Deadline", + "additionalUrgency": "Additional Urgency", + "additionalDeadline": "Additional Deadlines", + "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 is 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 Exam 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 Aid Detail (NI)", + "additionalAidOtherDetail": "Additional Aid Detail (Other)", + "additionalAidUrgency": "Additional Aid Urgency", + "additionalAidDeadlines": "Additional Aid Deadlines", + "willingToRepresent": "Willing to Represent?", + "notes": "General Notes", + "assignedUser": "Assigned Lawyer", + "teams": "Teams", + "createdAt": "Created At", + "modifiedAt": "Modified At", + "createdBy": "Created By", + "modifiedBy": "Modified By" + }, + "options": { + "status": { + "Draft": "Draft", + "Active": "Active", + "Closed": "Closed" + }, + "legalAidType": { + "": "", + "DirectAccess": "Direct Access", + "Regular": "Regular", + "Duty": "Duty" + }, + "aidType": { + "": "", + "הגשת ערעור": "Filing Appeal", + "יעוץ והדרכה": "Counseling & Guidance", + "סיוע נוסף ביטוח לאומי": "Additional NI Aid", + "סיוע נוסף תחום אחר": "Additional Aid (Other)", + "אי מתן סיוע": "No Aid Provided" + }, + "urgencyLevel": { + "": "", + "רגיל": "Normal", + "דחוף": "Urgent", + "בהול": "Critical" + }, + "additionalUrgency": { + "": "", + "רגיל": "Normal", + "דחוף": "Urgent", + "בהול": "Critical" + }, + "contactStatus": { + "": "", + "תקין": "Normal", + "לא תקין": "Abnormal" + }, + "courtName": { + "": "", + "בית הדין האזורי לעבודה ירושלים": "Regional Labor Court Jerusalem", + "בית הדין האזורי לעבודה תל אביב": "Regional Labor Court Tel Aviv", + "בית הדין האזורי לעבודה חיפה": "Regional Labor Court Haifa", + "בית הדין האזורי לעבודה באר שבע": "Regional Labor Court Be'er Sheva", + "בית הדין האזורי לעבודה נצרת": "Regional Labor Court Nazareth", + "בית הדין הארצי לעבודה": "National Labor Court" + }, + "proceedingSubject": { + "": "", + "נכות כללית": "General Disability", + "נכות מעבודה": "Work Disability", + "נכות איבה": "Hostility Disability", + "ניידות": "Mobility", + "אי כושר": "Incapacity", + "שירותים מיוחדים": "Special Services", + "סיעוד": "Nursing Care", + "גמלת הבטחת הכנסה": "Income Support", + "דמי אבטלה": "Unemployment Benefits", + "אחר": "Other" + }, + "recommendationType": { + "": "", + "ייעוץ והדרכה חלף ייצוג": "Counseling Instead of Representation", + "ויתור הלקוח": "Client Waiver", + "החלפת ייצוג": "Change Representation", + "לא נוצר קשר": "No Contact Made", + "לא התקיימה פגישה": "No Meeting Held", + "עדיין לא ניתן להחליט": "Cannot Decide Yet", + "נדרשים מסמכים נוספים": "Additional Documents Required", + "המלצה לסירוב": "Recommendation to Refuse" + }, + "additionalAidNeeded": { + "": "", + "לא נחוץ": "Not Needed", + "נחוץ בתחום ביטוח לאומי": "Needed (NI Field)", + "נחוץ בתחום אחר": "Needed (Other Field)" + }, + "additionalAidUrgency": { + "": "", + "רגיל": "Normal", + "דחוף": "Urgent", + "בהול": "Critical" + } + }, + "links": { + "case": "Case", + "contact": "Contact" + }, + "presetFilters": { + "draft": "Drafts", + "active": "Active", + "closed": "Closed" + } +} +``` + +### 4.9 i18n: Farsi/RTL wrapper (`fa_IR/LegalAid.json`) + +**Path:** `files/custom/Espo/Modules/LegalAssistance/Resources/i18n/fa_IR/LegalAid.json` + +> **This file is identical to `he_IL/LegalAid.json`.** EspoCRM uses `fa_IR` for RTL layout support. For Hebrew UIs, duplicate the `he_IL` content into `fa_IR`. + +(See section 4.7 above for the full content.) + +### 4.10 Custom Field View: `legal-aid-number.js` + +**Path:** `files/client/custom/modules/legal-assistance/src/views/case/fields/legal-aid-number.js` + +```javascript +define('legal-assistance:views/case/fields/legal-aid-number', + ['views/fields/varchar'], + function (Varchar) { + return class LegalAidNumberFieldView extends Varchar { + + detailTemplate = 'legal-assistance:case/fields/legal-aid-number/detail' + listTemplate = 'legal-assistance:case/fields/legal-aid-number/detail' + + _legalAidCount = 0 + _legalAidLoaded = false + + events = { + 'click [data-action="openLegalAidList"]': function (e) { + e.preventDefault(); + e.stopPropagation(); + this._openLegalAidList(); + } + } + + data() { + const data = super.data(); + data.hasLegalAid = this._legalAidCount > 0; + data.legalAidCount = this._legalAidCount; + return data; + } + + afterRender() { + super.afterRender(); + + if (this.isDetailMode() || this.isListMode()) { + if (!this._legalAidLoaded && this.model.get(this.name)) { + this._loadLegalAidCount(); + } + } + } + + _loadLegalAidCount() { + const caseId = this.model.id; + if (!caseId) return; + + Espo.Ajax.getRequest('LegalAid', { + where: [{ + type: 'equals', + attribute: 'caseId', + value: caseId + }], + select: 'id', + maxSize: 0 + }).then(response => { + this._legalAidLoaded = true; + this._legalAidCount = response.total || 0; + this.reRender(); + }); + } + + _openLegalAidList() { + const caseId = this.model.id; + const caseName = this.model.get('name'); + + const searchData = { + advanced: { + case: { + type: 'equals', + attribute: 'caseId', + value: caseId, + data: { + type: 'is', + idValue: caseId, + nameValue: caseName + } + } + } + }; + + // Write filter to localStorage so SearchManager picks it up + const storageKey = 'espo-listSearch-LegalAid'; + localStorage.setItem(storageKey, JSON.stringify(searchData)); + + this.getRouter().navigate('#LegalAid', {trigger: true}); + } + }; + } +); +``` + +**How this works:** +1. Extends the standard `varchar` field view. +2. On render, queries the `LegalAid` API to count records linked to this Case. +3. Displays the legal-aid number as a clickable link with a count badge `(N)`. +4. On click, writes a pre-built search filter to `localStorage` and navigates to `#LegalAid` -- the list view auto-applies the filter showing only records for this Case. + +### 4.11 Custom Field Template: `detail.tpl` + +**Path:** `files/client/custom/modules/legal-assistance/res/templates/case/fields/legal-aid-number/detail.tpl` + +```handlebars +{{#if value}}{{value}}{{#if hasLegalAid}} ({{legalAidCount}}){{/if}}{{else}}{{translate 'None'}}{{/if}} +``` + +### 4.12 Case Entity Extension: `entityDefs/Case.json` + +**Path:** `files/custom/Espo/Modules/LegalAssistance/Resources/metadata/entityDefs/Case.json` + +```json +{ + "fields": { + "cLegalAidNumber": { + "view": "legal-assistance:views/case/fields/legal-aid-number" + } + }, + "links": { + "legalAids": { + "type": "hasMany", + "entity": "LegalAid", + "foreign": "case", + "layoutRelationshipsDisabled": true + } + } +} +``` + +**What this does:** +- Overrides the `cLegalAidNumber` field on Case to use the custom view (making it clickable). +- Defines the `hasMany` side of the `Case <-> LegalAid` relationship. +- `layoutRelationshipsDisabled: true` prevents a default relationship panel from appearing (the custom field view handles navigation instead). + +### 4.13 Global Scope Registration: Hebrew (`he_IL/Global.json`) + +**Path:** `files/custom/Espo/Modules/LegalAssistance/Resources/i18n/he_IL/Global.json` + +```json +{ + "scopeNames": { + "LegalAid": "סיוע משפטי" + }, + "scopeNamesPlural": { + "LegalAid": "סיוע משפטי" + } +} +``` + +### 4.14 Global Scope Registration: English (`en_US/Global.json`) + +**Path:** `files/custom/Espo/Modules/LegalAssistance/Resources/i18n/en_US/Global.json` + +```json +{ + "scopeNames": { + "LegalAid": "Legal Aid" + }, + "scopeNamesPlural": { + "LegalAid": "Legal Aid Records" + } +} +``` + +### 4.15 SmartAssistant Tool: `GenerateInitialReport.php` + +**Path:** `files/custom/Espo/Modules/LegalAssistance/SmartAssistant/Tools/GenerateInitialReport.php` + +```php +log->info("LegalAssistance: Generating initial 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')) + : 'ללא איש קשר'; + + // Extract legal analysis from nested object + $legalAnalysis = $params['legalAnalysis'] ?? []; + if (is_string($legalAnalysis)) { + $legalAnalysis = json_decode($legalAnalysis, true) ?? []; + } + + // Find or create LegalAid + $legalAid = $this->entityManager + ->getRDBRepository('LegalAid') + ->where(['caseId' => $caseId]) + ->findOne(); + + if (!$legalAid) { + $legalAid = $this->entityManager->getNewEntity('LegalAid'); + } + + $legalAid->set([ + 'name' => "סיוע משפטי — {$contactName}", + 'status' => 'Active', + 'caseId' => $caseId, + 'contactId' => $contactId, + 'contactName' => $contactName, + 'assignedUserId' => $userId, + 'legalAidNumber' => $case->get('cLegalAidNumber') ?? '', + 'reportDate' => date('Y-m-d'), + + 'aidType' => $params['aidType'] ?? $legalAid->get('aidType'), + 'urgencyLevel' => $params['urgencyLevel'] ?? $legalAid->get('urgencyLevel'), + 'appointmentDate' => $this->normalizeDate($params['appointmentDate'] ?? null) ?? $legalAid->get('appointmentDate'), + 'filingDeadline' => $this->normalizeDate($params['filingDeadline'] ?? null) ?? $legalAid->get('filingDeadline'), + 'additionalUrgency' => $params['additionalUrgency'] ?? $legalAid->get('additionalUrgency'), + 'additionalDeadline' => $params['additionalDeadline'] ?? $legalAid->get('additionalDeadline'), + + 'firstContactDate' => $this->normalizeDate($params['firstContactDate'] ?? null) ?? $legalAid->get('firstContactDate'), + 'contactDelayReason' => $params['contactDelayReason'] ?? $legalAid->get('contactDelayReason'), + 'meetingDate' => $this->normalizeDate($params['meetingDate'] ?? null) ?? $legalAid->get('meetingDate'), + 'meetingDelayReason' => $params['meetingDelayReason'] ?? $legalAid->get('meetingDelayReason'), + 'meetingAttendees' => $params['meetingAttendees'] ?? $legalAid->get('meetingAttendees'), + 'contactStatus' => $params['contactStatus'] ?? $legalAid->get('contactStatus'), + 'contactStatusDetail' => $params['contactStatusDetail'] ?? $legalAid->get('contactStatusDetail'), + + 'proceedingNumber' => $params['proceedingNumber'] ?? $legalAid->get('proceedingNumber') ?? $case->get('cCourtCaseNumber'), + 'courtName' => $params['courtName'] ?? $legalAid->get('courtName'), + 'proceedingSubject' => $params['proceedingSubject'] ?? $legalAid->get('proceedingSubject'), + 'claimSummary' => $params['claimSummary'] ?? $legalAid->get('claimSummary'), + 'defenseClaims' => $params['defenseClaims'] ?? $legalAid->get('defenseClaims'), + + 'q1DocsReviewed' => $legalAnalysis['q1DocsReviewed'] ?? $legalAid->get('q1DocsReviewed') ?? false, + 'q1DocsDetail' => $legalAnalysis['q1DocsDetail'] ?? $legalAid->get('q1DocsDetail'), + 'q2Reasoned' => $legalAnalysis['q2Reasoned'] ?? $legalAid->get('q2Reasoned') ?? false, + 'q2ReasonDetail' => $legalAnalysis['q2ReasonDetail'] ?? $legalAid->get('q2ReasonDetail'), + 'q3PanelMatch' => $legalAnalysis['q3PanelMatch'] ?? $legalAid->get('q3PanelMatch') ?? false, + 'q4PanelComposition' => $legalAnalysis['q4PanelComposition'] ?? $legalAid->get('q4PanelComposition'), + 'q5ComplaintsAddressed' => $legalAnalysis['q5ComplaintsAddressed'] ?? $legalAid->get('q5ComplaintsAddressed') ?? false, + 'q6ComplaintsDetail' => $legalAnalysis['q6ComplaintsDetail'] ?? $legalAid->get('q6ComplaintsDetail'), + 'q7ClinicalExam' => $legalAnalysis['q7ClinicalExam'] ?? $legalAid->get('q7ClinicalExam') ?? false, + 'q8ClinicalGap' => $legalAnalysis['q8ClinicalGap'] ?? $legalAid->get('q8ClinicalGap'), + 'q9MoharaApplied' => $legalAnalysis['q9MoharaApplied'] ?? $legalAid->get('q9MoharaApplied') ?? false, + 'q9MoharaDetail' => $legalAnalysis['q9MoharaDetail'] ?? $legalAid->get('q9MoharaDetail'), + 'q10RehabGap' => $legalAnalysis['q10RehabGap'] ?? $legalAid->get('q10RehabGap') ?? false, + 'q10RehabDetail' => $legalAnalysis['q10RehabDetail'] ?? $legalAid->get('q10RehabDetail'), + 'q11ScoreGap' => $legalAnalysis['q11ScoreGap'] ?? $legalAid->get('q11ScoreGap') ?? false, + 'q11ScoreDetail' => $legalAnalysis['q11ScoreDetail'] ?? $legalAid->get('q11ScoreDetail'), + 'q12UniqueAspects' => $legalAnalysis['q12UniqueAspects'] ?? $legalAid->get('q12UniqueAspects') ?? false, + 'q12UniqueDetail' => $legalAnalysis['q12UniqueDetail'] ?? $legalAid->get('q12UniqueDetail'), + + 'recommendationType' => $params['recommendationType'] ?? $legalAid->get('recommendationType'), + 'recommendationDetail' => $params['recommendationDetail'] ?? $legalAid->get('recommendationDetail'), + + 'additionalAidNeeded' => $params['additionalAidNeeded'] ?? $legalAid->get('additionalAidNeeded'), + 'additionalAidNIDetail' => $params['additionalAidNIDetail'] ?? $legalAid->get('additionalAidNIDetail'), + 'additionalAidOtherDetail' => $params['additionalAidOtherDetail'] ?? $legalAid->get('additionalAidOtherDetail'), + 'additionalAidUrgency' => $params['additionalAidUrgency'] ?? $legalAid->get('additionalAidUrgency'), + 'additionalAidDeadlines' => $params['additionalAidDeadlines'] ?? $legalAid->get('additionalAidDeadlines'), + 'willingToRepresent' => $params['willingToRepresent'] ?? $legalAid->get('willingToRepresent') ?? false, + + 'notes' => $params['notes'] ?? $legalAid->get('notes'), + ]); + + $this->entityManager->saveEntity($legalAid); + + $this->log->info("LegalAssistance: Saved LegalAid {$legalAid->getId()} for case {$caseId}"); + + return [ + 'success' => true, + 'message' => "✅ רשומת סיוע משפטי נוצרה/עודכנה: \"{$contactName}\"", + 'entityType' => 'LegalAid', + 'entityId' => $legalAid->getId(), + ]; + } + + 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; + } +} +``` + +### 4.16 Checkbox Mapping for DOCX Templates: `addLegalAidData()` method + +**Source:** `LocalDocuments/files/custom/Espo/Modules/LocalDocuments/Services/LocalDocumentTemplateService.php` (last method in class) + +```php + private function addLegalAidData(array &$data, Entity $entity): void + { + // Computed aliases for template compatibility + $data['assignedUserName'] = $data['assignedUser.name'] ?? ''; + + if (empty($data['contactName'])) { + $firstName = $data['contact.firstName'] ?? ''; + $lastName = $data['contact.lastName'] ?? ''; + $data['contactName'] = trim($firstName . ' ' . $lastName); + } + + // Fallback legalAidNumber from Case + if (empty($data['legalAidNumber'])) { + $caseId = $entity->get('caseId'); + if ($caseId) { + $case = $this->entityManager->getEntityById('Case', $caseId); + if ($case) { + $data['legalAidNumber'] = $case->get('cLegalAidNumber') ?? ''; + } + } + } + + // Alias: legalAidNumber -> also as legalAidNumber (template uses ${legalAidNumber}) + // reportDate fallback to today + if (empty($data['reportDate'])) { + $data['reportDate'] = $data['today'] ?? $this->formatDate(date('Y-m-d')); + } + + $CHK = '☑'; + $UNCHK = '☐'; + + // urgencyLevel -> chk_urgency_* + $urgency = $entity->get('urgencyLevel') ?? ''; + $data['chk_urgency_regular'] = ($urgency === 'רגיל') ? $CHK : $UNCHK; + $data['chk_urgency_urgent'] = ($urgency === 'דחוף') ? $CHK : $UNCHK; + $data['chk_urgency_emergency'] = ($urgency === 'בהול') ? $CHK : $UNCHK; + + // aidType -> chk_aid_* + $aidType = $entity->get('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; + + // additionalUrgency -> chk_add_urgency_* + $addUrgency = $entity->get('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 -> chk_contact_* + $contactStatus = $entity->get('contactStatus') ?? ''; + $data['chk_contact_ok'] = ($contactStatus === 'תקין') ? $CHK : $UNCHK; + $data['chk_contact_bad'] = ($contactStatus === 'לא תקין') ? $CHK : $UNCHK; + + // recommendationType -> chk_rec_* + $rec = $entity->get('recommendationType') ?? ''; + $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; + + // additionalAidNeeded -> chk_no_additional, chk_additional_ni, chk_additional_other + $addAid = $entity->get('additionalAidNeeded') ?? ''; + $data['chk_no_additional'] = ($addAid === 'לא נחוץ') ? $CHK : $UNCHK; + $data['chk_additional_ni'] = ($addAid === 'נחוץ בתחום ביטוח לאומי') ? $CHK : $UNCHK; + $data['chk_additional_other'] = ($addAid === 'נחוץ בתחום אחר') ? $CHK : $UNCHK; + + // additionalAidUrgency -> chk_add_aid_* + $addAidUrgency = $entity->get('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; + + // willingToRepresent -> chk_willing_to_represent + $data['chk_willing_to_represent'] = $entity->get('willingToRepresent') ? $CHK : $UNCHK; + } +``` + +--- + +## 5. How to Register a New Entity (Checklist) + +Use this checklist when creating a new entity called `YourEntity` in module `YourModule`: + +- [ ] **1. `entityDefs/YourEntity.json`** -- Define all fields, links, collection defaults, and indexes. +- [ ] **2. `scopes/YourEntity.json`** -- Register the scope. Include `"isCustom": true` and `"module": "YourModule"`. +- [ ] **3. `clientDefs/YourEntity.json`** -- Set icon, color, filters, kanban config, and `"controller": "controllers/record"`. +- [ ] **4. `Controllers/YourEntity.php`** -- Create an empty PHP class extending `Espo\Core\Controllers\Record`. +- [ ] **5. `layouts/YourEntity/detail.json`** -- Define the detail view layout (panels with rows). +- [ ] **6. `layouts/YourEntity/list.json`** -- Define the list view columns. +- [ ] **7. `i18n/en_US/YourEntity.json`** -- English translations (scopeName, fields, options, links, presetFilters). +- [ ] **8. `i18n/he_IL/YourEntity.json`** -- Hebrew translations. +- [ ] **9. `i18n/fa_IR/YourEntity.json`** -- Copy of he_IL (RTL support). +- [ ] **10. `i18n/en_US/Global.json`** -- Add `scopeNames` and `scopeNamesPlural` entries. +- [ ] **11. `i18n/he_IL/Global.json`** -- Add Hebrew scope name entries. +- [ ] **12. Run `php rebuild.php`** (or Rebuild from admin UI) after deploying. +- [ ] **13. Set ACL roles** in Administration > Roles for the new entity. + +**Important file location rules:** +- Layouts: `Resources/layouts/YourEntity/` (NOT `metadata/layouts/`) +- Metadata: `Resources/metadata/entityDefs/`, `Resources/metadata/scopes/`, `Resources/metadata/clientDefs/` +- i18n: `Resources/i18n/{locale}/` +- Controllers: `Controllers/` + +--- + +## 6. How to Link an Entity to Case (with Custom Field View) + +This pattern turns a plain varchar field on Case (like `cLegalAidNumber`) into a clickable link that navigates to a filtered list of the linked entity. + +### Step 1: Define the relationship in both entityDefs + +**In `entityDefs/YourEntity.json` (belongsTo side):** +```json +{ + "fields": { + "case": { + "type": "link", + "required": true + } + }, + "links": { + "case": { + "type": "belongsTo", + "entity": "Case", + "foreign": "yourEntities" + } + } +} +``` + +**In `entityDefs/Case.json` (hasMany side, in your module):** +```json +{ + "fields": { + "cSomeField": { + "view": "your-module:views/case/fields/your-field-view" + } + }, + "links": { + "yourEntities": { + "type": "hasMany", + "entity": "YourEntity", + "foreign": "case", + "layoutRelationshipsDisabled": true + } + } +} +``` + +### Step 2: Create the JS field view + +**File:** `files/client/custom/modules/your-module/src/views/case/fields/your-field-view.js` + +Pattern: +1. Extend `views/fields/varchar`. +2. Set `detailTemplate` to your custom Handlebars template. +3. In `afterRender()`, query your entity's API to get a count. +4. On click, write a search filter to `localStorage` and navigate to `#YourEntity`. + +(See section 4.10 for the full working example.) + +### Step 3: Create the Handlebars template + +**File:** `files/client/custom/modules/your-module/res/templates/case/fields/your-field/detail.tpl` + +```handlebars +{{#if value}}{{value}}{{#if hasRecords}} ({{recordCount}}){{/if}}{{else}}{{translate 'None'}}{{/if}} +``` + +--- + +## 7. How to Add Checkbox Mapping for DOCX Templates + +When generating DOCX documents from templates, enum fields need to be converted to checkbox characters so the template shows checked/unchecked boxes. + +### Pattern + +```php +$CHK = "\u{2611}"; // ☑ (checked) +$UNCHK = "\u{2610}"; // ☐ (unchecked) + +// For each enum option, create a named placeholder: +$fieldValue = $entity->get('yourEnumField') ?? ''; +$data['chk_prefix_option1'] = ($fieldValue === 'Option 1 Value') ? $CHK : $UNCHK; +$data['chk_prefix_option2'] = ($fieldValue === 'Option 2 Value') ? $CHK : $UNCHK; +$data['chk_prefix_option3'] = ($fieldValue === 'Option 3 Value') ? $CHK : $UNCHK; + +// For bool fields: +$data['chk_my_bool'] = $entity->get('myBoolField') ? $CHK : $UNCHK; +``` + +### Naming Convention + +| Enum Field | Placeholder Pattern | +|---|---| +| `urgencyLevel` | `chk_urgency_regular`, `chk_urgency_urgent`, `chk_urgency_emergency` | +| `aidType` | `chk_aid_appeal`, `chk_aid_consulting`, ... | +| `recommendationType` | `chk_rec_consulting`, `chk_rec_waiver`, ... | +| `additionalAidNeeded` | `chk_no_additional`, `chk_additional_ni`, `chk_additional_other` | + +### In the DOCX Template + +Use PHPWord placeholders like `${chk_urgency_regular}` in the template. When the document is generated, these get replaced with the checkbox character. + +### Integration Point + +Add a method like `addYourEntityData()` in the `LocalDocumentTemplateService` (or your own service class) and call it when the entity type matches: + +```php +if ($entity->getEntityType() === 'YourEntity') { + $this->addYourEntityData($data, $entity); +} +``` + +(See section 4.16 for the complete working implementation.) diff --git a/files/custom/Espo/Modules/LegalAssistance/Controllers/LegalAid.php b/files/custom/Espo/Modules/LegalAssistance/Controllers/LegalAid.php deleted file mode 100644 index b3b48be..0000000 --- a/files/custom/Espo/Modules/LegalAssistance/Controllers/LegalAid.php +++ /dev/null @@ -1,9 +0,0 @@ -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 + $assignedUser = $userId ? $this->entityManager->getEntityById('User', $userId) : null; + $assignedUserName = $assignedUser ? $assignedUser->get('name') : ($this->user->get('name') ?? ''); + + // 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 + $templatePath = $this->resolveTemplatePath(); + $documentName = "דוח גישה ישירה — {$contactName}"; + $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); + + // Create Attachment + $attachment = $this->entityManager->getNewEntity('Attachment'); + $attachment->set([ + 'name' => $documentName . '.docx', + 'type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'role' => 'Attachment', + 'size' => strlen($content), + 'relatedType' => 'Document', + ]); + $this->entityManager->saveEntity($attachment); + file_put_contents('data/upload/' . $attachment->getId(), $content); + + // Create Document linked to Case + $document = $this->entityManager->getNewEntity('Document'); + $document->set([ + 'name' => $documentName, + 'fileId' => $attachment->getId(), + 'fileName' => $documentName . '.docx', + 'assignedUserId' => $userId, + 'type' => 'דוח', + ]); + $this->entityManager->saveEntity($document); + + // Link Document to Case + $this->entityManager->getRDBRepository('Document') + ->getRelation($document, 'cases') + ->relate($case); + + $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) ?? []; + } + + return [ + // Header + 'legalAidNumber' => $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 = '☑'; + $UNCHK = '☐'; + + $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_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 + { + // Look up template via DocumentTemplate entity + $template = $this->entityManager + ->getRDBRepository('DocumentTemplate') + ->where(['name' => self::TEMPLATE_NAME]) + ->findOne(); + + if ($template) { + $fileId = $template->get('fileId'); + if ($fileId) { + $filePath = 'data/upload/' . $fileId; + if (file_exists($filePath)) { + return $filePath; + } + } + + // Try templatePath field + $templatePath = $template->get('templatePath'); + if ($templatePath && file_exists($templatePath)) { + return $templatePath; + } + } + + // Fallback: look in known locations + $fallbackPaths = [ + 'custom/Espo/Modules/LegalAssistance/templates/direct-access-report.docx', + __DIR__ . '/../../../../../../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 processTemplate(string $inputPath, string $outputPath, array $data): void + { + $processor = new TemplateProcessor($inputPath); + + foreach ($data as $key => $value) { + if ($value === null) { + $value = ''; + } + if (is_array($value)) { + $value = implode(', ', $value); + } elseif (is_bool($value)) { + $value = $value ? 'כן' : 'לא'; + } + $processor->setValue($key, (string) $value); + } + + $processor->saveAs($outputPath); + } + + private function 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; + } +} diff --git a/files/custom/Espo/Modules/LegalAssistance/SmartAssistant/Tools/GenerateInitialReport.php b/files/custom/Espo/Modules/LegalAssistance/SmartAssistant/Tools/GenerateInitialReport.php deleted file mode 100644 index b6a53f6..0000000 --- a/files/custom/Espo/Modules/LegalAssistance/SmartAssistant/Tools/GenerateInitialReport.php +++ /dev/null @@ -1,150 +0,0 @@ -log->info("LegalAssistance: Generating initial 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')) - : 'ללא איש קשר'; - - // Extract legal analysis from nested object - $legalAnalysis = $params['legalAnalysis'] ?? []; - if (is_string($legalAnalysis)) { - $legalAnalysis = json_decode($legalAnalysis, true) ?? []; - } - - // Find or create LegalAid - $legalAid = $this->entityManager - ->getRDBRepository('LegalAid') - ->where(['caseId' => $caseId]) - ->findOne(); - - if (!$legalAid) { - $legalAid = $this->entityManager->getNewEntity('LegalAid'); - } - - $legalAid->set([ - 'name' => "סיוע משפטי — {$contactName}", - 'status' => 'Active', - 'caseId' => $caseId, - 'contactId' => $contactId, - 'contactName' => $contactName, - 'assignedUserId' => $userId, - 'legalAidNumber' => $case->get('cLegalAidNumber') ?? '', - 'reportDate' => date('Y-m-d'), - - 'aidType' => $params['aidType'] ?? $legalAid->get('aidType'), - 'urgencyLevel' => $params['urgencyLevel'] ?? $legalAid->get('urgencyLevel'), - 'appointmentDate' => $this->normalizeDate($params['appointmentDate'] ?? null) ?? $legalAid->get('appointmentDate'), - 'filingDeadline' => $this->normalizeDate($params['filingDeadline'] ?? null) ?? $legalAid->get('filingDeadline'), - 'additionalUrgency' => $params['additionalUrgency'] ?? $legalAid->get('additionalUrgency'), - 'additionalDeadline' => $params['additionalDeadline'] ?? $legalAid->get('additionalDeadline'), - - 'firstContactDate' => $this->normalizeDate($params['firstContactDate'] ?? null) ?? $legalAid->get('firstContactDate'), - 'contactDelayReason' => $params['contactDelayReason'] ?? $legalAid->get('contactDelayReason'), - 'meetingDate' => $this->normalizeDate($params['meetingDate'] ?? null) ?? $legalAid->get('meetingDate'), - 'meetingDelayReason' => $params['meetingDelayReason'] ?? $legalAid->get('meetingDelayReason'), - 'meetingAttendees' => $params['meetingAttendees'] ?? $legalAid->get('meetingAttendees'), - 'contactStatus' => $params['contactStatus'] ?? $legalAid->get('contactStatus'), - 'contactStatusDetail' => $params['contactStatusDetail'] ?? $legalAid->get('contactStatusDetail'), - - 'proceedingNumber' => $params['proceedingNumber'] ?? $legalAid->get('proceedingNumber') ?? $case->get('cCourtCaseNumber'), - 'courtName' => $params['courtName'] ?? $legalAid->get('courtName'), - 'proceedingSubject' => $params['proceedingSubject'] ?? $legalAid->get('proceedingSubject'), - 'claimSummary' => $params['claimSummary'] ?? $legalAid->get('claimSummary'), - 'defenseClaims' => $params['defenseClaims'] ?? $legalAid->get('defenseClaims'), - - 'q1DocsReviewed' => $legalAnalysis['q1DocsReviewed'] ?? $legalAid->get('q1DocsReviewed') ?? false, - 'q1DocsDetail' => $legalAnalysis['q1DocsDetail'] ?? $legalAid->get('q1DocsDetail'), - 'q2Reasoned' => $legalAnalysis['q2Reasoned'] ?? $legalAid->get('q2Reasoned') ?? false, - 'q2ReasonDetail' => $legalAnalysis['q2ReasonDetail'] ?? $legalAid->get('q2ReasonDetail'), - 'q3PanelMatch' => $legalAnalysis['q3PanelMatch'] ?? $legalAid->get('q3PanelMatch') ?? false, - 'q4PanelComposition' => $legalAnalysis['q4PanelComposition'] ?? $legalAid->get('q4PanelComposition'), - 'q5ComplaintsAddressed' => $legalAnalysis['q5ComplaintsAddressed'] ?? $legalAid->get('q5ComplaintsAddressed') ?? false, - 'q6ComplaintsDetail' => $legalAnalysis['q6ComplaintsDetail'] ?? $legalAid->get('q6ComplaintsDetail'), - 'q7ClinicalExam' => $legalAnalysis['q7ClinicalExam'] ?? $legalAid->get('q7ClinicalExam') ?? false, - 'q8ClinicalGap' => $legalAnalysis['q8ClinicalGap'] ?? $legalAid->get('q8ClinicalGap'), - 'q9MoharaApplied' => $legalAnalysis['q9MoharaApplied'] ?? $legalAid->get('q9MoharaApplied') ?? false, - 'q9MoharaDetail' => $legalAnalysis['q9MoharaDetail'] ?? $legalAid->get('q9MoharaDetail'), - 'q10RehabGap' => $legalAnalysis['q10RehabGap'] ?? $legalAid->get('q10RehabGap') ?? false, - 'q10RehabDetail' => $legalAnalysis['q10RehabDetail'] ?? $legalAid->get('q10RehabDetail'), - 'q11ScoreGap' => $legalAnalysis['q11ScoreGap'] ?? $legalAid->get('q11ScoreGap') ?? false, - 'q11ScoreDetail' => $legalAnalysis['q11ScoreDetail'] ?? $legalAid->get('q11ScoreDetail'), - 'q12UniqueAspects' => $legalAnalysis['q12UniqueAspects'] ?? $legalAid->get('q12UniqueAspects') ?? false, - 'q12UniqueDetail' => $legalAnalysis['q12UniqueDetail'] ?? $legalAid->get('q12UniqueDetail'), - - 'recommendationType' => $params['recommendationType'] ?? $legalAid->get('recommendationType'), - 'recommendationDetail' => $params['recommendationDetail'] ?? $legalAid->get('recommendationDetail'), - - 'additionalAidNeeded' => $params['additionalAidNeeded'] ?? $legalAid->get('additionalAidNeeded'), - 'additionalAidNIDetail' => $params['additionalAidNIDetail'] ?? $legalAid->get('additionalAidNIDetail'), - 'additionalAidOtherDetail' => $params['additionalAidOtherDetail'] ?? $legalAid->get('additionalAidOtherDetail'), - 'additionalAidUrgency' => $params['additionalAidUrgency'] ?? $legalAid->get('additionalAidUrgency'), - 'additionalAidDeadlines' => $params['additionalAidDeadlines'] ?? $legalAid->get('additionalAidDeadlines'), - 'willingToRepresent' => $params['willingToRepresent'] ?? $legalAid->get('willingToRepresent') ?? false, - - 'notes' => $params['notes'] ?? $legalAid->get('notes'), - ]); - - $this->entityManager->saveEntity($legalAid); - - $this->log->info("LegalAssistance: Saved LegalAid {$legalAid->getId()} for case {$caseId}"); - - return [ - 'success' => true, - 'message' => "✅ רשומת סיוע משפטי נוצרה/עודכנה: \"{$contactName}\"", - 'entityType' => 'LegalAid', - 'entityId' => $legalAid->getId(), - ]; - } - - 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; - } -} diff --git a/manifest.json b/manifest.json index 760ca75..c552b00 100644 --- a/manifest.json +++ b/manifest.json @@ -1,11 +1,11 @@ { "name": "LegalAssistance", - "version": "1.5.0", + "version": "2.0.0", "acceptableVersions": [">=8.0.0"], "php": [">=8.1"], "releaseDate": "2026-04-07", "author": "Marcus-Law", - "description": "סיוע משפטי — ישות LegalAid מרכזית, דוח גישה ישירה, משימות אוטומטיות, אינטגרציה עם שירה", + "description": "סיוע משפטי — יצירת דוח גישה ישירה (JSON ישירות ל-DOCX), אינטגרציה עם שירה", "scripts": { "afterInstall": "scripts/AfterInstall.php", "afterUninstall": "scripts/AfterUninstall.php" diff --git a/plugins/legal-assistance/tools.json b/plugins/legal-assistance/tools.json index 4615d70..8117d87 100644 --- a/plugins/legal-assistance/tools.json +++ b/plugins/legal-assistance/tools.json @@ -1,7 +1,7 @@ { "tools": { - "generate_initial_report": { - "description": "Generate Direct Access initial report (דוח דיווח ראשוני — גישה ישירה). Call ONLY after collecting ALL required data through conversation with the lawyer. This tool creates/updates a LegalAid entity and generates a DOCX document from it.", + "generate_direct_access_report": { + "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": { "type": "object", "properties": { @@ -148,39 +148,6 @@ }, "required": ["aidType", "proceedingSubject", "recommendationType"] } - }, - "update_legal_aid": { - "description": "Update fields on an existing LegalAid entity. Use this to fill or modify any field on the legal aid record. Pass only the fields you want to update — existing values are preserved for unspecified fields. You can update any combination of fields.", - "parameters": { - "type": "object", - "properties": { - "legalAidId": { - "type": "string", - "description": "ID of the LegalAid entity to update" - }, - "fields": { - "type": "object", - "description": "Key-value pairs of fields to update. Any field from the LegalAid entity is valid: aidType, urgencyLevel, appointmentDate, filingDeadline, firstContactDate, contactDelayReason, meetingDate, meetingDelayReason, meetingAttendees, contactStatus, contactStatusDetail, proceedingNumber, courtName, proceedingSubject, claimSummary, defenseClaims, q1DocsReviewed, q1DocsDetail, q2Reasoned, q2ReasonDetail, q3PanelMatch, q4PanelComposition, q5ComplaintsAddressed, q6ComplaintsDetail, q7ClinicalExam, q8ClinicalGap, q9MoharaApplied, q9MoharaDetail, q10RehabGap, q10RehabDetail, q11ScoreGap, q11ScoreDetail, q12UniqueAspects, q12UniqueDetail, recommendationType, recommendationDetail, additionalAidNeeded, additionalAidNIDetail, additionalAidOtherDetail, additionalAidUrgency, additionalAidDeadlines, willingToRepresent, notes, status, contactName, legalAidNumber, reportDate, additionalUrgency, additionalDeadline" - } - }, - "required": ["legalAidId", "fields"] - } - }, - "get_legal_aid": { - "description": "Read an existing LegalAid entity and return all its field values. Use this to check what data has already been filled before updating.", - "parameters": { - "type": "object", - "properties": { - "legalAidId": { - "type": "string", - "description": "ID of the LegalAid entity to read" - }, - "caseId": { - "type": "string", - "description": "Alternatively, find the LegalAid by Case ID" - } - } - } } } } diff --git a/scripts/AfterInstall.php b/scripts/AfterInstall.php index 31808c8..41e33ab 100644 --- a/scripts/AfterInstall.php +++ b/scripts/AfterInstall.php @@ -153,7 +153,7 @@ class AfterInstall $template = $entityManager->getNewEntity('DocumentTemplate'); $template->set([ 'name' => 'דוח גישה ישירה', - 'entityType' => 'LegalAid', + 'entityType' => 'Case', 'fileId' => $attachment->getId(), ]); $entityManager->saveEntity($template); diff --git a/scripts/AfterUninstall.php b/scripts/AfterUninstall.php index a21aea9..3a739b6 100644 --- a/scripts/AfterUninstall.php +++ b/scripts/AfterUninstall.php @@ -3,11 +3,15 @@ * LegalAssistance Extension — AfterUninstall * * 1. Remove plugin files from ai-gateway - * 2. Clear cache + * 2. Drop LegalAid and DirectAccessReport DB tables (with warning log) + * 3. Remove LegalAssistance-specific columns from Case table + * 4. Remove DocumentTemplate records created by this extension + * 5. Clear cache ************************************************************************/ use Espo\Core\Container; use Espo\Core\DataManager; +use Espo\Core\ORM\EntityManager; use Espo\Core\Utils\Log; class AfterUninstall @@ -16,10 +20,40 @@ class AfterUninstall { /** @var Log $log */ $log = $container->getByClass(Log::class); + /** @var EntityManager $entityManager */ + $entityManager = $container->getByClass(EntityManager::class); $log->info('LegalAssistance: Running AfterUninstall...'); - // Remove ai-gateway plugin files + // 1. Remove ai-gateway plugin files + $this->removePluginFiles($log); + + // 2. Drop tables created by this extension + $this->dropTables($entityManager, $log); + + // 3. Remove LegalAssistance-specific columns from Case + $this->cleanCaseColumns($entityManager, $log); + + // 4. Remove DocumentTemplate records + $this->removeDocumentTemplates($entityManager, $log); + + // 5. Remove custom i18n files + $this->removeCustomI18n($log); + + // 6. Clear cache + try { + $dataManager = $container->getByClass(DataManager::class); + $dataManager->clearCache(); + $log->info('LegalAssistance: Cache cleared.'); + } catch (\Throwable $e) { + $log->warning('LegalAssistance: Cache clear failed: ' . $e->getMessage()); + } + + $log->info('LegalAssistance: AfterUninstall completed.'); + } + + private function removePluginFiles(Log $log): void + { $gatewayPaths = [ '/home/chaim/ai-gateway/plugins/legal-assistance', '/opt/ai-gateway/plugins/legal-assistance', @@ -37,15 +71,104 @@ class AfterUninstall $log->info("LegalAssistance: Removed plugin files from {$dir}"); } } + } - // Clear cache - try { - $dataManager = $container->getByClass(DataManager::class); - $dataManager->clearCache(); - } catch (\Throwable $e) { - $log->warning('LegalAssistance: Cache clear failed: ' . $e->getMessage()); + 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()); + } } + } - $log->info('LegalAssistance: AfterUninstall completed.'); + 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}"); + } + } } }