Projects — Developer Guide
Lean Operations module mirroring Tasks / Opportunities patterns (board, stats, assignee, notes, domain timeline) with members, date-only schedule fields, and soft CRM / Task links. Prefer copying those patterns over inventing new ones. Field is title (not name).
Backend layout
| Piece | Path |
|---|---|
| Models | app/Models/Project.php, ProjectMember, ProjectNote, ProjectActivity, ProjectMilestone |
| Enums | ProjectStatusEnum, ProjectActivityTypeEnum, ProjectMilestoneStatusEnum |
| Service | app/Services/Tenant/ProjectService.php |
| Controller | app/Http/Controllers/Tenant/Api/V1/ProjectController.php |
| Requests | app/Http/Requests/Tenant/Api/V1/Project/* |
| Resources | app/Http/Resources/Tenant/Api/V1/Project/* |
| Policy | app/Policies/ProjectPolicy.php |
| Events | app/Events/Project*.php |
| Subscriber | app/Listeners/ProjectEventSubscriber.php (audit + assignee / member notifications) |
| Notifications | ProjectAssignedNotification, ProjectMemberAddedNotification |
| Link rules | LinkableContact, LinkableCompanyForOpportunity, LinkableOpportunityForProject, EligibleProjectAssignee, LinkableProjectMilestone; Tasks uses LinkableProject |
| Dashboard | DashboardWidgetService — active_projects, overdue_projects |
| Factories | ProjectFactory, ProjectNoteFactory, ProjectActivityFactory, ProjectMilestoneFactory |
| Tests | tests/Feature/Tenant/Project/ProjectTest.php, ProjectMilestoneTest.php, ProjectGanttTest.php, ProjectHeatmapTest.php |
| Migrations | 2026_08_14_100000… — projects tables; 2026_09_16_122010 project_milestones; catalog bumps through 1.6.0 (workload heatmap); Tasks milestone_id + task_dependencies with Tasks 1.5.0 |
Domain notes
- No hard module dependency: Projects has no
module_dependenciesrow — installable standalone.contact_id/company_id/opportunity_idare nullable soft links. - Status machine on
ProjectStatusEnum::allowedTransitions():planned → active|cancelled,active → on_hold|completed|cancelled,on_hold → active|cancelled,completed/cancelledterminal.changeStatus()throwsValidationException(422,status) for disallowed transitions. - New projects always start as
planned. Default assignee is the creator whenassigned_tois omitted (or when the actor lacksprojects.assign). - Visibility without
projects.assign(and not superadmin):assigned_toORcreated_byORproject_members(Project::isVisibleTo()/ProjectService::applyVisibilityScope()). Withprojects.assign, org-wide. - Assignee is not stored as a member —
normalizeMemberIds()strips the assignee id frommember_ids. PUT /projects/{project}/membersacceptsmember_ids: [](present|array) to clear all members.- Assignee/member pickers:
GET /usersexcludes the authenticated user (Users admin list). The Projects form and record page merge the signed-in user into picker options (same pattern as Departments) so a solo owner can assign themselves. starts_on/ends_onaredatecasts. Overdue = open status + non-nullends_on+ends_onbefore workspace “today” (TenantSettingService::applyRuntimeConfigsonow()->toDateString()is workspace TZ).- Soft Task link: nullable
tasks.project_idFK →projects(nullOnDelete). Validated byLinkableProject(Projects entitled + project visible to actor). Optionaltasks.milestone_id→project_milestones(nullOnDelete) viaLinkableProjectMilestone(same project). Same-project task dependencies intask_dependencies(cycle rejected). Catalog bump Tasks 1.5.0. - Customer Portal share (1.9.0):
portal_visible(defaultfalse) — requirescontact_idorcompany_id. Portal lists only shared projects in Contact/Company scope. Clearing share sets all linked tasks’visible_to_portaltofalse. - Milestones: nested under projects (
project_milestones: title, description,due_on, statusopen|completed,sort_order,completed_at). CRUD +POST …/complete. Timeline typesmilestone_created/updated/completed/deleted. Show resource embedsmilestoneswhen loaded. - Portfolio Gantt:
GET /projects/gantt— same filters/visibility as list; rows include project dates, milestones, and soft Tasks when Tasks is entitled + actor cantasks.viewandvieweach task.depends_on_task_idsintersected with visible task ids. Daterangeis min/max of project/milestone/task dates. Limit default 100 (max 200). Clears list default eager loads. Catalog projects 1.5.0. - Workload heatmap:
GET /projects/heatmap— assignee × week cells from open projects (schedule overlap) + soft project Tasks (due week) when entitled; pressure score/band mirrors CRM Analytics staff pressure idea. Optionalweeks1–16 (default 8) from workspace start-of-week. Catalog projects 1.6.0. - Calendar projection:
ProjectServiceupserts all-day Calendar events onstarts_on/ends_onwhen Calendar is entitled (sourceproject). projects.force.deleteis not granted to any default role — owner/superadmin only.
Permissions
projects.view | create | update | delete | restore | force.delete | assignRoutes use module:projects then can:projects.* / policies.
Catalog: slug projects, category operations, is_default_included = false, is_billable = false, sort_order = 10, version 1.9.0. Registered via DefaultModuleRegistrar migration (migrate-only) — no module_dependencies row.
API (tenant)
Base: /api/tenant/v1 — full reference tenant-v1-projects.md.
| Method | Path | Permission |
|---|---|---|
| GET | /projects | view |
| GET | /projects/stats | view |
| GET | /projects/board | view |
| GET | /projects/gantt | view |
| POST | /projects | create |
| GET | /projects/{project} | view |
| PUT | /projects/{project} | update |
| DELETE | /projects/{project} | delete |
| POST | /projects/{project}/restore | restore |
| DELETE | /projects/{project}/force | force.delete |
| POST | /projects/{project}/assign | assign |
| PUT | /projects/{project}/members | assign |
| POST | /projects/{project}/status | update |
| POST | /projects/{project}/notes | update |
| GET | /projects/{project}/timeline | view |
| GET | /projects/{project}/milestones | view |
| POST | /projects/{project}/milestones | update |
| PUT | /projects/{project}/milestones/{milestone} | update |
| POST | /projects/{project}/milestones/{milestone}/complete | update |
| DELETE | /projects/{project}/milestones/{milestone} | update |
Frontend
SPA should mirror Tasks (board default + list + Gantt + Heatmap, create/edit page, record page) under AppLayout — do not invent a parallel shell. Nav: Operations.
| Piece | Path (expected) |
|---|---|
| Page | src/pages/projects/ (board + list + Gantt + Heatmap) |
| Gantt | src/pages/projects/projects-gantt.tsx |
| Heatmap | src/pages/projects/projects-heatmap.tsx |
| Shared board | src/components/crm/kanban-board.tsx (per-column vertical scroll + contained horizontal scroll; titles stay fixed) |
| Form / detail | create/edit page + record page (overview, members, notes, timeline) |
| Service | projectService in src/api/services.ts (gantt, heatmap) |
| Types | Project* / ProjectGantt* / ProjectHeatmap* in src/types/api.ts; Task gains optional project_id / project |
| Query keys | QUERY_KEYS.projects / project(id) / projectTimeline(id) / projectStats / projectBoard / projectGantt / projectHeatmap |
| Permissions | PERMISSIONS.projects.* |
| Nav | permission: projects.view, module: 'projects' — Operations sidebar group |
| Route | tenantRoutes.projects = '/projects', RequireAccess module="projects" |
| Notifications | project.assigned, project.member_added → /projects?project={id} |
| Playwright | e2e/pages/projects.page.ts, e2e/tests/projects/, npm run test:e2e:projects |
| Realtime hook | src/hooks/use-projects-board-realtime.ts — Kanban-only debounce-invalidate |
Real-time board sync
Mirrors Help Desk 1.12.0 / Tasks 1.8.0. Channel tenant.{tenantId}.projects.board (ProjectBoardChannel: same tenant + projects.view). Broadcasts ProjectCreated / ProjectUpdated / ProjectStatusChanged / ProjectAssigned / ProjectDeleted via SafeRealtimeBroadcast. Status-changed uses the existing ProjectStatusChanged domain event. SPA hook is board-view only (Gantt/Heatmap do not join). Catalog 1.7.0 → 1.8.0. Readiness: Projects 1.8.0.
Tests
php artisan test --compact tests/Feature/Tenant/Project/
npm run typecheck && npm run lint && npm run build
npm run test:e2e:projectsLogging
- Spatie
LogsActivityonProject(log nameprojects) - Domain
project_activitiestimeline (created,updated,assigned,members_synced,status_changed,note_added,milestone_*,deleted,restored) PlatformAuditServiceviaProjectEventSubscriber
Ask EloSync
Ask EloSync Project tools (existing get_project / search / overdue plus confirmed status/assign/note writes) are registered in AIToolRegistry and confirmed via PendingAiActionService. Status uses ProjectService::changeStatus (same as HTTP POST …/status). Get payload includes assigned_to (user id) and assignee_name. See AI tools and AI Project triage production readiness.
Deferred
- Project tags and
PRJ-numbers - Project tags,
PRJ-numbers