CRM tools and tasks

What it is

Two things sharing one subsystem:

The activity timeline — notes, tasks, files, emails, batch emails and system events attached to a target. For supplier pages the target is the supplier key, so the CRM panel beside the supplier workspace is the supplier’s history.

The task system — the staff work queue. Tasks have an assignee (user or group), priority, status and due date, and the sidebar shows live counters for "your tasks" and "group tasks".

Where it lives

Concern Location

Routes

/tasks/list/:filterId/filter (crm-tools/tasks-routing.routes.ts); /emails/* reuses crm-tools/emails/ components — see emails

Components

crm-tools/ — crm-sidebar/, actions-list/, actions-list-pinned/, actions-add/, actions-add-fabs/, notes/, tasks/, files/, emails/, comments/, events/

Store

crm-tools/store/ — the crm slice

Services

crm-tools/services/ — crm.service.ts, tasks.service.ts, tasks-filters.service.ts, comments.service.ts, emails.service.ts, emails-status.service.ts, file-types.service.ts, action-tab-calculator.service.ts (each with a -mock sibling)

Models

crm-tools/models/ — base-crm.model.ts, note.model.ts, task.model.ts, file.model.ts, email.model.ts, event.model.ts, batch.model.ts, comment.model.ts, document.model.ts, crm-user.model.ts, meta.model.ts, crm-item.type.ts, task-filter.model.ts, and the *-data.model.ts write shapes

Counters

shared/task-count-badge/

Backends

The CRM spans three hosts, which is why CrmService is the most header-heavy service in the app:

Base URL Used for

actionsToolsUrl

activity/, notes/, files/, tasks/, tasks/filters/counts/ — the durable CRM store.

eventLoggerUrl

es-changelog — the system event stream, read from Elasticsearch.

messagingUrl

Outbound email.

Headers

CrmService maintains its headers by subscribing to the store in its constructor:

this.headers['X-InstanceId']              = '' + appConfig.env.instanceId;   // 8001
this.headers['X-EventLogger-InstanceId']  = '' + appConfig.env.instanceId;

this.store.select(selectAuthToken).subscribe((jwt) => this.headers['Authorization'] = jwt);
this.store.select('crm').subscribe((data) => {
  if (data.targetUuid) { this.headers['X-Actions-TargetUUID'] = data.targetUuid; }
});

Two consequences:

  • The current target UUID is implicit in every request. A call issued while the target is changing can be scoped to the wrong entity.

  • For the event-logger call, listActionsEvents copies the header across: X-EventLogger-TargetUUID = X-Actions-TargetUUID, then deletes the original.

Authorization here is the raw JWT with no scheme — in addition to the X-Session-Token header that AuthInterceptor adds. Actions Tools accepts both.

The normalised store

CrmState is a hand-built normalised store:

interface CrmStateModel {
  crmItems: { [uuid: string]: CrmItem };   // the entity map
  all?: string[];                          // ordered uuid lists — the "streams"
  pinnedNotes?: string[];
  notes?: string[];
  emails?: string[];
  batchEmails?: string[];
  tasks?: string[];
  events?: string[];
  files?: string[];
}

interface CrmState {
  targetUuid: string;
  crmCurrentState: CrmStateModel;                      // the active target
  crmStatesList: { [targetUuid: string]: CrmStateModel }; // per-target cache
  crmPendingItems: { [key: string]: CrmItemStatus };   // in-flight mutations
}

Selectors are generated per stream by a factory:

export const selectCrmAll         = createTimelineStreamSelector('all');
export const selectCrmNotes       = createTimelineStreamSelector('notes');
export const selectCrmFiles       = createTimelineStreamSelector('files');
export const selectCrmTasks       = createTimelineStreamSelector('tasks');
export const selectCrmEvents      = createTimelineStreamSelector('events');
export const selectCrmEmails      = createTimelineStreamSelector('emails');
export const selectCrmBatchEmails = createTimelineStreamSelector('batchEmails');
export const selectCrmPinnedNotes = createTimelineStreamSelector('pinnedNotes');

Reducer behaviour

Action Behaviour

SET_TARGET_UUID

Switches crmCurrentState to the cached state for that target, creating an empty one if absent. Dispatched automatically by SupplierEffects.navigateToSupplier alongside the supplier load.

UPDATE_CRM_MODEL

Merges per key. For array values it concatenates and de-duplicates (append semantics, for paging); for object values it spreads. alterKeys names the keys that should be replaced instead of merged.

ADD_CRM_MODEL

Inserts into crmItems and unshift`s the uuid onto each stream in `updateStreams — new items appear at the top.

DELETE_CRM_MODEL

Removes from crmItems and from all, notes, tasks, events, emails, pinnedNotes.

EDIT_CRM_NOTE / PIN_CRM_NOTE

Marks crmPendingItems[uuid] = PENDING.

CRM_ITEM_RESPONSE_SUCCESS / _ERROR / CRM_ITEM_CLEAR_STATUS

Set and clear crmPendingItems[actionId] with CrmItemStatus (SUCCESS / ERROR / PENDING).

CRM_CLEAR_CURRENT_STATE

Resets only the active target’s state.

ADD_CRM_MODEL and DELETE_CRM_MODEL mutate stateToUpdate.crmItems and stateToUpdate[stream] in place before spreading the surrounding object. The crmStatesList entry and crmCurrentState can therefore reference the same mutated inner objects. Do not rely on reference identity of a stream array to detect change.

DELETE_CRM_MODEL also omits files and batchEmails from its cleanup, so deleting a file leaves its uuid in the files stream pointing at a removed item.

Item types

A single activity/ feed returns mixed types, dispatched on item.type in CrmService.listActions:

type Model

note

NoteModel

task

TaskModel

event

EventModel

file

FileModel

email, email_supplier

EmailModel

batch

EmailModel

anything else

BaseCrmModel (graceful degradation)

CrmItem (models/crm-item.type.ts) is the union. Write shapes are separate models: NoteDataModel, FileDataModel, EventDataModel.

Adding a type means touching the switch, the CrmItem union, CrmStateModel’s stream list and the `DELETE_CRM_MODEL cleanup.

Timeline endpoints

Endpoint Notes

GET activity/

The combined feed. limit (default 5) and last for cursor paging.

GET notes/, GET files/

Per-type feeds via a shared listActionsBase<T> helper, default limit 10.

GET es-changelog (event logger)

The event stream. Requests limit = 2000000 with a // todo - ugly fix - check and fix it later comment — there is no real paging on events.

POST/PUT/DELETE notes/…

Note CRUD, including pinning.

POST/PUT/GET/DELETE tasks/…

Task CRUD — see below.

Paging uses an opaque last cursor, JSON-stringified into a query parameter, with the next cursor returned in CrmMetaModel. Infinite scroll uses ngx-infinite-scroll and appends via `UPDATE_CRM_MODEL’s de-duplicating merge.

Tasks

TasksService on actionsToolsUrl:

Endpoint Purpose

GET tasks/

List with filters.

POST tasks/

Create (Serialize(data, TaskModel)).

GET tasks/:uuid/

Read.

PUT tasks/:uuid/

Update.

DELETE tasks/:uuid/

Delete one.

DELETE tasks/ with { uuids: […​] } body

Bulk delete — issued via httpClient.request('delete', …, { body }).

PATCH tasks/

Bulk partial update.

GET tasks/:uuid/events/

A task’s own event history.

Status and priority

export enum TaskStatuses  { NEW = 'new', IN_PROGR = 'in-progr', DONE = 'done', CLOSED = 'closed' }
export enum TaskPriorities { LOW = 'low', MEDIUM = 'medium', HIGH = 'high', CRITICAL = 'critical' }

Both ship with parallel display arrays (taskStatuses, taskPriorities) mapping key to label — note IN_PROGR displays as "Started", not "In progress".

tasks/task-next-status.directive.ts implements the forward transition, so status progression is encoded in a directive rather than a state machine.

Task save hooks

CRM_BEFORE_TASK_SAVE and CRM_AFTER_TASK_SAVE bracket a task save. They exist so that other parts of the app can react to a task changing without coupling to the save call — for example refreshing counters.

Task filters and counters

FilterSet (models/task-filter.model.ts) describes a named filter:

class FilterSet {
  title: string;
  isGroup? = false;
  count?: number;
  filters?:      { [key: string]: any };   // UI representation
  filtersQuery?: { [key: string]: any };   // query representation
  subFilters?: any[];
  permCode?: USER_PERMISSION | string;      // filter visible only with this permission
  membersCount?: number;
}

FilterDefinition splits them into predefined and custom. The /tasks route takes a :filterId (default 0), so a filter selection is a URL, and therefore bookmarkable and shareable.

TasksFiltersService hits tasks/filters/counts/ and tasks/filters/counts/assignees/.

TaskCountBadgeComponent (shared/task-count-badge/) renders the sidebar counters with filterType of currentUser or currentUserGroup. Its behaviour:

  • Reloads every RELOAD_COUNT_INTERVAL = 60000 ms.

  • Listens to document visibilitychange and only reloads while the tab is visible.

  • Also reloads on the TASK_COUNTERS_UPDATE event from TableEventsService, which CrmEffects broadcasts after a task mutation.

  • Consults FeatureFlagsService — TASKS_MID_VER_SIMPLE_QUERY switches the counting query.

  • ViewEncapsulation.None, so its styles are global.

Notes, files, comments, emails

Item Detail

Notes

notes/ plus actions-list-pinned/. Pinning is a note-level flag (PIN_CRM_NOTE), with a dedicated pinnedNotes stream. Bulk note import is available under Manage — see bulk actions.

Files

files/ with file-types.service.ts, file-type.model.ts, file-type-category.model.ts. Delete carries both a uuid and a b2bUuid, because the file record and the document blob live in different systems.

Comments

comments/ with comments.service.ts — comments on CRM items, distinct from notes.

Emails

emails/ with emails.service.ts and emails-status.service.ts. Single email (SEND_SINGLE_EMAIL) and batch email are separate streams. See emails.

Events

events/ — read-only system events from the event logger. See events.

B2B preview

crm-tools/shared/b2b-preview/ renders an attached document inline.

Attachment base

crm-tools/shared/item-with-attachment.component.ts.

action-tab-calculator.service.ts (on internalApiUrl) computes the per-tab counts shown on the CRM panel’s tab strip.

CRM panel presentation

The panel has four states — HIDDEN, OPEN, CLOSED, FIXED (CrmStates) — held in the layout slice and defaulted per route pattern: fixed on the supplier General tab, closed on other supplier tabs, hidden on the supplier list. See UI composition.

CRM_OPEN_ALL_TAB on TableEventsService lets effects switch the panel to the "All" tab after adding an item.

Access

/tasks and /emails both deny REST_UW_ACCESS and ADVANCE_AUDITORS, and the sidebar hides the task counters for those groups. CRM_ADMIN is the elevated CRM permission. Filters can carry their own permCode, so a filter can be hidden from users who could otherwise see the list.

Gotchas

Issue Detail

Implicit target scoping

The target UUID travels as a header set from a store subscription, not as a parameter.

Event paging is a limit=2000000

Acknowledged debt in the source.

DELETE_CRM_MODEL misses two streams

files and batchEmails are not cleaned up.

In-place mutation

ADD_CRM_MODEL / DELETE_CRM_MODEL mutate nested state.

IN_PROGR displays as "Started"

Do not "correct" the label without checking the training material.

Bulk delete uses a request body

DELETE tasks/ with { uuids }.

Pending statuses must be cleared

CRM_ITEM_CLEAR_STATUS after handling; otherwise a stale status persists for that actionId.

crmItems starts null

initialState().crmCurrentState.crmItems is null, not {}. ADD_CRM_MODEL initialises it lazily; other readers must guard.