This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
LegalAssistance/docs/examples/entity-reference-legalaid.md
chaim 21cf570cbc feat!: v2.0.0 — replace LegalAid entity with direct JSON-to-DOCX generation
Breaking change: LegalAid entity removed entirely. Reports are now
generated directly from JSON params to DOCX without any intermediate
entity or database table.

New:
- DirectAccessReportGenerator: takes JSON from Shira, generates DOCX
  via PHPWord, attaches as Document to Case (single PHP file)
- AfterUninstall: full cleanup script (drops tables, columns, templates)
- docs/examples/entity-reference-legalaid.md: full reference for future
  entity creation

Removed:
- LegalAid entity (entityDefs, scopes, clientDefs, layouts, i18n, controller)
- GenerateInitialReport tool (replaced by DirectAccessReportGenerator)
- Custom JS field view for cLegalAidNumber
- update_legal_aid and get_legal_aid tools
- Case link to legalAids

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:12:28 +00:00

58 KiB

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

{
    "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

{
    "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

{
    "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

namespace Espo\Modules\LegalAssistance\Controllers;

use Espo\Core\Controllers\Record;

class LegalAid extends Record
{
}

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

[
    {
        "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

[
    {"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

{
    "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

{
    "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.)

Path: files/client/custom/modules/legal-assistance/src/views/case/fields/legal-aid-number.js

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

{{#if value}}<a href="javascript:" data-action="openLegalAidList" title="הצג רשומות סיוע משפטי">{{value}}</a>{{#if hasLegalAid}} <span class="text-muted small">({{legalAidCount}})</span>{{/if}}{{else}}<span class="none-value">{{translate 'None'}}</span>{{/if}}

4.12 Case Entity Extension: entityDefs/Case.json

Path: files/custom/Espo/Modules/LegalAssistance/Resources/metadata/entityDefs/Case.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

{
    "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

{
    "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

namespace Espo\Modules\LegalAssistance\SmartAssistant\Tools;

use Espo\Core\Exceptions\Error;
use Espo\Core\InjectableFactory;
use Espo\ORM\EntityManager;
use Espo\Core\Utils\Log;

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}");

        $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)

    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/

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):

{
    "fields": {
        "case": {
            "type": "link",
            "required": true
        }
    },
    "links": {
        "case": {
            "type": "belongsTo",
            "entity": "Case",
            "foreign": "yourEntities"
        }
    }
}

In entityDefs/Case.json (hasMany side, in your module):

{
    "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

{{#if value}}<a href="javascript:" data-action="openYourEntityList" title="Show records">{{value}}</a>{{#if hasRecords}} <span class="text-muted small">({{recordCount}})</span>{{/if}}{{else}}<span class="none-value">{{translate 'None'}}</span>{{/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

$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:

if ($entity->getEntityType() === 'YourEntity') {
    $this->addYourEntityData($data, $entity);
}

(See section 4.16 for the complete working implementation.)