Skip to content

Tasks — Developer Guide ​

Mirror of the Leads developer guide. Prefer copying Leads patterns over inventing new ones.

Backend layout ​

PiecePath
Modelsapp/Models/Task.php, TaskTag, TaskNote, TaskNoteMention, TaskActivity, TaskDependency
Enumsapp/Enums/Tenant/TaskStatusEnum (includes waiting; Open label To Do), TaskPriorityEnum, TaskActivityTypeEnum
Serviceapp/Services/Tenant/TaskService.php (+ ScopesToAssignee), TaskTagService.php
Controllerapp/Http/Controllers/Tenant/Api/V1/TaskController.php, TaskTagController.php
Requestsapp/Http/Requests/Tenant/Api/V1/Task/*
Resourcesapp/Http/Resources/Tenant/Api/V1/Task/*
Policyapp/Policies/TaskPolicy.php (changeDueDate → tasks.change_due_date), TaskTagPolicy.php
Eventsapp/Events/Task*.php (includes TaskTagCreated, TaskTagsSynced)
Subscriberapp/Listeners/TaskEventSubscriber.php (audit + notifications)
Notificationsapp/Notifications/Tenant/Task/* — assign/complete/reopen: database + optional mail (+ webpush on assign); mentions: database + webpush + optional mail; due/overdue: database; daily digest: mail only (TaskDueDigestNotification)
MentionsApp\Support\NoteMentions, NoteMentionService; wired from TaskNoteAdded in TaskEventSubscriber
Digest deliverytask_digest_deliveries + TaskDigestDeliveryService; TrackTaskDueDigestDelivery on NotificationSent / NotificationFailed
Scheduled duecrm:send-due-notifications every 5 minutes (onOneServer); tenant setting task_reminder_time (Daily Reminder Time) gated in workspace timezone — see Workspace timezone convention
Teststests/Feature/Tenant/Task/TaskTest.php, TaskTagTest.php, TaskDependencyTest.php, tests/Feature/Tenant/Notification/TaskDueDigestNotificationTest.php, NoteMentionNotificationTest.php, tests/Unit/NoteMentionsTest.php

Domain notes ​

  • Comment bodies may include @[Display Name](user:ID) mention tokens (composer UI shows @Name chips). On TaskNoteAdded, NoteMentionService persists task_note_mentions and sends task.mentioned (skip self; idempotent via dedupe_key). Mail is optional via email_notifications.task_mentioned (default off).
  • Assignee scoping via ScopesToAssignee with tasks.assign.
  • Task assignee pickers use filterTaskAssigneeOptions (omit suspended only). They do not reuse filterLeadAssigneeOptions — exclude_from_lead_auto_assign and workspace owners remain assignable on Tasks. Backend create/update/assign use EligibleTaskAssignee (User::isEligibleAssignee — rejects suspended users; allows keeping the current assignee when suspended so they can be cleared).
  • Updating due_at after create requires tasks.change_due_date (enforced in TaskService / policy). Initial due_at on create is allowed without that permission.
  • due_at is a UTC instant (UtcDateTime / UtcIso). Overdue / due-today / due-this-week SQL uses App\Support\UtcInstant so non-UTC workspace timezones do not mark upcoming tasks overdue. SPA create/edit uses appLocalInputToIso / isoToAppLocalInput (Settings → General timezone), not raw datetime-local / ISO slice.
  • Board columns are one per TaskStatusEnum case.
  • Optional soft project_id (nullable FK → projects, nullOnDelete) validated by LinkableProject — Projects module must be entitled and the project must be visible to the actor. Optional milestone_id (FK → project_milestones, nullOnDelete) via LinkableProjectMilestone (requires project_id and same project). Optional depends_on_task_ids[] syncs task_dependencies (same project; self/cycle rejected). Clearing project_id clears milestone and dependencies. Customer Portal (1.9.0): visible_to_portal (default false) only when the parent project is portal_visible; clearing project_id forces it off. List/show embed project (includes portal_visible), milestone, and depends_on_task_ids when loaded. Catalog version 1.9.0. See Projects developer guide.
  • Calendar overlay: open tasks with due_at project onto Calendar (source task, organizer = assignee or creator) when Calendar is entitled; completed/cancelled/cleared due date removes the projection.
  • Attachments: task_attachments and task_note_attachments on the workspace uploads disk (tenants/{uuid}/tasks/). Types/sizes from workspace storage.upload_policy via WorkspaceUploadPolicy. Multi-file batches assert total bytes against remaining Storage quota before any object is written. Auth’d download/delete; bytes count toward Storage used. Production checklist: upload policy + task media readiness.

Permissions ​

config/tenant-permissions.php:

tasks.view | create | update | delete | assign | complete | change_due_date

Routes use module:tasks then can:tasks.* / policies.

API (tenant) ​

Base: /api/tenant/v1 — full reference tenant-v1-tasks.md.

MethodPathPermission
GET/tasksview
GET/tasks/statsview
GET/tasks/boardview
POST/taskscreate
GET/tasks/{task}view
PUT/tasks/{task}update (+ change_due_date when changing due_at)
DELETE/tasks/{task}delete
POST/tasks/{task}/assignassign
POST/tasks/{task}/completecomplete
POST/tasks/{task}/reopencomplete
POST/tasks/{task}/notesupdate
GET/tasks/{task}/timelineview
GET/task-tagsview
POST/task-tagscreate
PUT/task-tags/{taskTag}owner (superadmin)
DELETE/task-tags/{taskTag}owner (superadmin)
PUT/tasks/{task}/tagsupdate

Auth login/me include modules: string[] for SPA gating.

Colored tags are create-only for MVP (no tag update/delete/reorder routes). Assign on store/update via tag_ids[] or PUT …/tags; filter list/board with tag_id.

Real-time board sync ​

Mirrors the Leads 1.8.0 / Opportunities 1.3.0 / Live Chat inbox invalidate pattern — not the Team Chat message-patching pattern. The board refetches on change instead of patching individual cards. Columns are TaskStatusEnum values, not pipeline stages.

  • Channel: private tenant.{tenantId}.tasks.board, registered in routes/channels.php. Auth (TaskBoardChannel::join): same tenant and tasks.view.
  • Broadcast events (App\Events\Task\*Broadcast, all ShouldBroadcastNow, kept separate from the existing domain events App\Events\TaskCreated etc.):
    • TaskCreatedBroadcast → broadcastAs('TaskCreated')
    • TaskUpdatedBroadcast → broadcastAs('TaskUpdated')
    • TaskStatusChangedBroadcast → broadcastAs('TaskStatusChanged') (includes previous_status; fired from handleTaskUpdated when before/after status differ — no extra domain event)
    • TaskAssignedBroadcast → broadcastAs('TaskAssigned') (includes assigned_to)
    • TaskDeletedBroadcast → broadcastAs('TaskDeleted')
  • Payload shape (TaskBoardBroadcastPayload): action, task_id, uuid, actor_id, status, plus previous_status (status-changed only) / assigned_to (assigned only).
  • Dispatch: TaskEventSubscriber fires the matching broadcast from create/update/assign/delete handlers via SafeRealtimeBroadcast::dispatch so a Reverb outage never breaks the write path.
  • Frontend: src/hooks/use-tasks-board-realtime.ts joins the channel via shared window.Echo while tasks-page.tsx has viewMode === 'board'. Listens for .TaskCreated / .TaskUpdated / .TaskStatusChanged / .TaskAssigned / .TaskDeleted and debounces (~200ms) an invalidate of QUERY_KEYS.taskBoard + QUERY_KEYS.taskStats.
  • Catalog tasks 1.7.0 → 1.8.0 (migrate-only + CatalogSeeder companion).
  • Production readiness: Tasks real-time board sync 1.8.0.

Frontend ​

PiecePath
Pagesrc/pages/tasks/tasks-page.tsx (board default + list; list row menu Complete/Reopen via tasks.complete)
Formtask-form-dialog.tsx
Detailtask-view-page.tsx (full page) + task-record-sheet.tsx (list peek: Overview / Comments / Activity). Both Comments composers use MentionComposer; board DnD auto-saves status on the list page
Shared boardsrc/components/crm/kanban-board.tsx (per-column vertical scroll + contained horizontal scroll; titles stay fixed; touch pan on column lists)
Mentions UIsrc/components/crm/mention-composer.tsx (shows @Name chips; emits @[Name](user:id); keep typing after pick; Backspace/Delete removes chips), src/lib/note-mentions.ts (formatNoteMentionsForDisplay in record pages + latest-note-follow-up.tsx list/board previews)
Notification registrysrc/notifications/modules/tasks.ts (task.mentioned)
ServicetaskService in src/api/services.ts
Realtime board hooksrc/hooks/use-tasks-board-realtime.ts — joins tenant.{id}.tasks.board, debounce-invalidates board + stats
Navpermission: tasks.view, module: 'tasks'
Catalog1.10.1 (human-readable status timing)

Status timing (1.10.0 / 1.10.1) ​

  • Column tasks.started_at (nullable UTC) set on first enter to in_progress; cleared on reopen with completed_at
  • Activity type status_changed with properties.before/after.status + duration_seconds
  • TaskService::timing() builds created_to_completed, started_to_completed, segments, time_in_status (current cycle after latest reopened)
  • SPA: TaskTimingSummary on view Activity + list peek overview; durations as compact human text (45m, 3h, 1d 2h 5m) via DurationFormat::human / formatDurationHuman

Tests ​

bash
# Backend
php artisan test --compact tests/Feature/Tenant/Task/TaskTest.php
php artisan test --compact tests/Feature/Tenant/Task/TaskStatusTimingTest.php
php artisan test --compact tests/Feature/Tenant/Task/TaskRealtimeBoardTest.php
php artisan test --compact tests/Feature/Tenant/Task/TaskTagTest.php
php artisan test --compact tests/Unit/UtcInstantTest.php

# Frontend E2E
npm run test:e2e:tasks

Logging ​

  • Spatie LogsActivity on Task (log name tasks)
  • Domain task_activities timeline (status_changed for status hops)
  • PlatformAuditService via TaskEventSubscriber (includes task_tag_created, task_tags_synced)

Intentional differences from Leads ​

LeadsTasks
Stages + independent statusStatus + priority enums (no stages)
Follow-upsNone
Assignment history tableTimeline only
export / convertcomplete / change_due_date
Board by stageBoard by status

AI tools ​

Ask EloSync Task tools (get_task, confirmed status/assign/note writes, plus existing list reads and create_task) are registered in AIToolRegistry and confirmed via PendingAiActionService. See AI tools and AI Task triage production readiness.

Official documentation for the EloSync SaaS Platform.