Skip to content

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 ​

PiecePath
Modelsapp/Models/Project.php, ProjectMember, ProjectNote, ProjectActivity, ProjectMilestone
EnumsProjectStatusEnum, ProjectActivityTypeEnum, ProjectMilestoneStatusEnum
Serviceapp/Services/Tenant/ProjectService.php
Controllerapp/Http/Controllers/Tenant/Api/V1/ProjectController.php
Requestsapp/Http/Requests/Tenant/Api/V1/Project/*
Resourcesapp/Http/Resources/Tenant/Api/V1/Project/*
Policyapp/Policies/ProjectPolicy.php
Eventsapp/Events/Project*.php
Subscriberapp/Listeners/ProjectEventSubscriber.php (audit + assignee / member notifications)
NotificationsProjectAssignedNotification, ProjectMemberAddedNotification
Link rulesLinkableContact, LinkableCompanyForOpportunity, LinkableOpportunityForProject, EligibleProjectAssignee, LinkableProjectMilestone; Tasks uses LinkableProject
DashboardDashboardWidgetService — active_projects, overdue_projects
FactoriesProjectFactory, ProjectNoteFactory, ProjectActivityFactory, ProjectMilestoneFactory
Teststests/Feature/Tenant/Project/ProjectTest.php, ProjectMilestoneTest.php, ProjectGanttTest.php, ProjectHeatmapTest.php
Migrations2026_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_dependencies row — installable standalone. contact_id / company_id / opportunity_id are nullable soft links.
  • Status machine on ProjectStatusEnum::allowedTransitions(): planned → active|cancelled, active → on_hold|completed|cancelled, on_hold → active|cancelled, completed/cancelled terminal. changeStatus() throws ValidationException (422, status) for disallowed transitions.
  • New projects always start as planned. Default assignee is the creator when assigned_to is omitted (or when the actor lacks projects.assign).
  • Visibility without projects.assign (and not superadmin): assigned_to OR created_by OR project_members (Project::isVisibleTo() / ProjectService::applyVisibilityScope()). With projects.assign, org-wide.
  • Assignee is not stored as a member — normalizeMemberIds() strips the assignee id from member_ids.
  • PUT /projects/{project}/members accepts member_ids: [] (present|array) to clear all members.
  • Assignee/member pickers: GET /users excludes 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_on are date casts. Overdue = open status + non-null ends_on + ends_on before workspace “today” (TenantSettingService::applyRuntimeConfig so now()->toDateString() is workspace TZ).
  • Soft Task link: nullable tasks.project_id FK → projects (nullOnDelete). Validated by LinkableProject (Projects entitled + project visible to actor). Optional tasks.milestone_id → project_milestones (nullOnDelete) via LinkableProjectMilestone (same project). Same-project task dependencies in task_dependencies (cycle rejected). Catalog bump Tasks 1.5.0.
  • Customer Portal share (1.9.0): portal_visible (default false) — requires contact_id or company_id. Portal lists only shared projects in Contact/Company scope. Clearing share sets all linked tasks’ visible_to_portal to false.
  • Milestones: nested under projects (project_milestones: title, description, due_on, status open|completed, sort_order, completed_at). CRUD + POST …/complete. Timeline types milestone_created / updated / completed / deleted. Show resource embeds milestones when loaded.
  • Portfolio Gantt: GET /projects/gantt — same filters/visibility as list; rows include project dates, milestones, and soft Tasks when Tasks is entitled + actor can tasks.view and view each task. depends_on_task_ids intersected with visible task ids. Date range is 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. Optional weeks 1–16 (default 8) from workspace start-of-week. Catalog projects 1.6.0.
  • Calendar projection: ProjectService upserts all-day Calendar events on starts_on/ends_on when Calendar is entitled (source project).
  • projects.force.delete is not granted to any default role — owner/superadmin only.

Permissions ​

projects.view | create | update | delete | restore | force.delete | assign

Routes 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.

MethodPathPermission
GET/projectsview
GET/projects/statsview
GET/projects/boardview
GET/projects/ganttview
POST/projectscreate
GET/projects/{project}view
PUT/projects/{project}update
DELETE/projects/{project}delete
POST/projects/{project}/restorerestore
DELETE/projects/{project}/forceforce.delete
POST/projects/{project}/assignassign
PUT/projects/{project}/membersassign
POST/projects/{project}/statusupdate
POST/projects/{project}/notesupdate
GET/projects/{project}/timelineview
GET/projects/{project}/milestonesview
POST/projects/{project}/milestonesupdate
PUT/projects/{project}/milestones/{milestone}update
POST/projects/{project}/milestones/{milestone}/completeupdate
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.

PiecePath (expected)
Pagesrc/pages/projects/ (board + list + Gantt + Heatmap)
Ganttsrc/pages/projects/projects-gantt.tsx
Heatmapsrc/pages/projects/projects-heatmap.tsx
Shared boardsrc/components/crm/kanban-board.tsx (per-column vertical scroll + contained horizontal scroll; titles stay fixed)
Form / detailcreate/edit page + record page (overview, members, notes, timeline)
ServiceprojectService in src/api/services.ts (gantt, heatmap)
TypesProject* / ProjectGantt* / ProjectHeatmap* in src/types/api.ts; Task gains optional project_id / project
Query keysQUERY_KEYS.projects / project(id) / projectTimeline(id) / projectStats / projectBoard / projectGantt / projectHeatmap
PermissionsPERMISSIONS.projects.*
Navpermission: projects.view, module: 'projects' — Operations sidebar group
RoutetenantRoutes.projects = '/projects', RequireAccess module="projects"
Notificationsproject.assigned, project.member_added → /projects?project={id}
Playwrighte2e/pages/projects.page.ts, e2e/tests/projects/, npm run test:e2e:projects
Realtime hooksrc/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 ​

bash
php artisan test --compact tests/Feature/Tenant/Project/
npm run typecheck && npm run lint && npm run build
npm run test:e2e:projects

Logging ​

  • Spatie LogsActivity on Project (log name projects)
  • Domain project_activities timeline (created, updated, assigned, members_synced, status_changed, note_added, milestone_*, deleted, restored)
  • PlatformAuditService via ProjectEventSubscriber

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

Official documentation for the EloSync SaaS Platform.