Lattice Grid Buy a licence

demo D263

Support Desk Board: One Ticket Stream, Many Queues in Angular

One ticket stream, a live lane per queue and an open-tickets chart, with no backend

createDataRouter · MockWebSocket

This is a ready-made service desk: one ticket stream drives a live lane per queue and an open-tickets chart, with no backend. It shows a support view built from a single feed, ready to connect to a real ticketing source.

Building…
Loading a live grid…

This is the Angular version. A standalone component takes the whole configuration through one config input, surfaces grid events as outputs, and exposes the live grid on a getter for anything the inputs do not cover.

The configuration

import { Component, signal, inject, OnDestroy } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { createGrid, createHeadlessGrid, type Grid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
import { createDataRouter } from '@toclocoinc/lattice-grid/modules/data-router';
import { MockWebSocket } from '@toclocoinc/lattice-grid/modules/mock-socket';
import { LatticeGridComponent, LatticeChartComponent, LatticeRouter, provideLattice, provideLatticeRouter } from '@toclocoinc/lattice-grid/angular';

// The vendored mock-socket module ships no ready-made desk feed, so this one
// is written here, to the same contract MockWebSocket expects.
function* deskFeed(): Generator<any> {
  const queues = ['billing', 'technical', 'onboarding'];
  const agents = ['Ana', 'Bo', 'Cy', 'Dee'];
  let nextId = 1;
  const ticket = (queue: string) => ({
    id: 'K-' + String(1000 + nextId++), queue, subject: 'Ticket ' + nextId,
    priority: ['low', 'normal', 'high'][Math.floor(Math.random() * 3)],
    agent: agents[Math.floor(Math.random() * agents.length)],
    waitMins: Math.round(Math.random() * 90),
  });
  const rows: any[] = [];
  for (const q of queues) for (let i = 0; i < 6; i++) rows.push(ticket(q));
  yield { kind: 'snapshot', rows };
  while (true) {
    if (Math.random() < 0.3 && rows.length) {
      const gone = rows.splice(Math.floor(Math.random() * rows.length), 1)[0];
      yield { kind: 'delta', changes: [{ op: 'delete', row: { id: gone.id } }] };
    } else {
      const row = ticket(queues[Math.floor(Math.random() * queues.length)]);
      rows.push(row);
      yield { kind: 'delta', changes: [{ op: 'upsert', row }] };
    }
  }
}

const COLUMNS = [
  { field: 'id', title: 'Ticket', layout: { width: 92 } },
  { field: 'subject', title: 'Subject', layout: { flex: 1, min: 120 } },
  { field: 'priority', title: 'Priority', filter: { type: 'set' } },
  { field: 'agent', title: 'Agent', filter: { type: 'set' } },
  { field: 'waitMins', title: 'Wait', type: 'number', total: 'max' },
];

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [LatticeGridComponent, LatticeChartComponent],
  // Partition on the queue: a resolved ticket leaves its lane as a delete, a
  // new one drops in as an upsert, with no refresh.
  providers: [provideLatticeRouter({ key: 'queue', rowKey: 'id' })],
  template:
    '<div style="display:grid;grid-template-columns:repeat(3, 1fr);gap:14px">' +
    '  <lattice-grid route="billing" [config]="laneConfig" style="display:block;height:320px"></lattice-grid>' +
    '  <lattice-grid route="technical" [config]="laneConfig" style="display:block;height:320px"></lattice-grid>' +
    '  <lattice-grid route="onboarding" [config]="laneConfig" style="display:block;height:320px"></lattice-grid>' +
    '</div>' +
    '@if (metricsGrid(); as m) {' +
    '  <lattice-chart [grid]="m" [config]="chartConfig" style="display:block;height:200px;margin-top:14px"></lattice-chart>' +
    '}',
})
export class AppComponent implements OnDestroy {
  private routerService = inject(LatticeRouter);
  private socket = new MockWebSocket({ feed: deskFeed(), rate: 1100, jitter: 300 });

  metricsGrid = signal<Grid | null>(null);
  chartConfig = { type: 'area', x: 't', y: 'open', title: 'Open tickets across the desk' };
  laneConfig = { rowKey: 'id', selection: 'single', highlightOnChange: { duration: 400 }, columns: COLUMNS };

  constructor() {
    const metrics = createHeadlessGrid({ rowKey: 't', columns: [
      { field: 't', type: 'number' }, { field: 'open', type: 'number' },
    ] });
    this.routerService.attach(metrics, 'metric', { rowKey: 't' });
    this.metricsGrid.set(metrics);

    this.socket.onmessage = (event: any) => {
      const message = JSON.parse(event.data);
      if (message.kind === 'snapshot') this.routerService.router.load(message.rows);
      else this.routerService.router.apply(message.changes);
    };
  }

  ngOnDestroy() { this.socket.close(); }
}

bootstrapApplication(AppComponent, {
  providers: [provideLattice({ createGrid, createChart, createDataRouter })],
});