# Hotline KPI thresholds

> How the hotline object stores its six KPI rows, how Target / Min / Avg / Max feed wallboard banding, and what the admin procedures behind the Hotlines screen accept.

A **hotline** groups calls for routing and reporting, and carries the KPI
thresholds that the live wallboard reads when it tints a tile green, amber
or red. You manage hotlines in the TeleQuick console under
**Admin → Hotlines**. Every field on that screen maps to a column on the
`dim_hotline` row, and every read or write goes through an
`adminHotlines.*` procedure.

## The hotline object

A hotline row has four identity fields and eighteen KPI fields.

| Field          | Type                                                                        | Notes                                                        |
| -------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `id`           | string                                                                      | Server-assigned. Shown in the drawer header when editing.    |
| `code`         | string                                                                      | Short handle, rendered monospace in the list. See validation below. |
| `name`         | string                                                                      | Human label, e.g. `Premium · English`.                       |
| `hotline_type` | `inbound` \| `outbound` \| `blended` \| `service` \| `sales` \| `collections` | Fixed set. Chosen from a dropdown; no free text.           |
| `active`       | boolean                                                                     | Soft-delete flag. Inactive hotlines are hidden from the list by default. |

The KPI fields are all nullable. A null renders as `—` in the list and in
the KPI matrix, and means "no threshold configured for this row".

The list and detail payloads differ. `adminHotlines.list` returns only the
identity fields plus the six **target** values (and `sla_window_sec`), which
is exactly what the table columns show. The Min / Avg / Max values come back
only from `adminHotlines.get`, which is why the drawer loads the row again
when you click it.

## The six KPI rows and their units

| KPI row    | Target field     | Band fields                                        | Unit                     |
| ---------- | ---------------- | -------------------------------------------------- | ------------------------ |
| SLA        | `sla_target_pct` | `sla_min_pct`, `sla_avg_pct`, `sla_max_pct`         | percent, within a window |
| SLA window | `sla_window_sec` | —                                                   | seconds                  |
| ASA        | `asa_target_sec` | `asa_min_sec`, `asa_avg_sec`, `asa_max_sec`         | seconds                  |
| AHT        | `aht_target_sec` | `aht_min_sec`, `aht_max_sec`                        | seconds                  |
| FTE        | `fte_target`     | `fte_min`, `fte_max`                                | headcount (unitless)     |
| Occupancy  | `occ_target_pct` | `occ_min_pct`, `occ_max_pct`                        | percent                  |

SLA is the only row with a second target-side input: `sla_window_sec` is the
answer window that `sla_target_pct` is measured against. The list column
renders the pair together — a hotline with `sla_target_pct = 80` and
`sla_window_sec = 20` shows as `80/20s`. If either half is null the column
shows `—`, even if the other half is set.

For the industry definitions of these metrics, see
[Telephony Metrics](/glossary/metrics).

## How Target, Min, Avg and Max become bands

The console states the contract directly on the screen:

> Target is the goal. Min / Avg / Max define the green-amber-red bands the
> live wallboard uses for this hotline.

Two consequences matter when you edit these fields:

- **Target is not a band edge.** It is the goal you publish; the tint comes
  from the Min / Avg / Max values on the same row. You can set a target with
  no bands, in which case the wallboard has nothing to tint against for that
  KPI.
- **The Hotlines screen does not evaluate the bands.** It persists the
  numbers as entered. There is no cross-field check in the form — it will
  happily save a Min above a Max, or bands that do not bracket the target.
  Getting the ordering right for each KPI's direction (higher-is-better for
  SLA and Occupancy, lower-is-better for ASA and AHT) is your responsibility.

Because the bands live on the hotline and not on the dashboard, two
wallboards pointed at the same hotline tint identically, and changing a
threshold here changes every wallboard that reads it.

## Why AHT, FTE and Occupancy have no Avg

The KPI matrix renders an Avg cell only where the underlying row has an
`_avg` column. SLA and ASA do (`sla_avg_pct`, `asa_avg_sec`); AHT, FTE and
Occupancy do not. Those three rows are stored as a Target plus a Min/Max
pair only, so they support a two-edge band rather than a three-edge one.
The Avg cell for those rows is absent from the form — it is not disabled
input you can enable later, and there is no field to write to.

## Defaults for a new hotline

**New hotline** opens the form pre-filled. These are starting points the
form suggests, not platform limits, and every one is editable before you
save:

| Row        | Target        | Min | Avg | Max |
| ---------- | ------------- | --- | --- | --- |
| SLA        | 80% / 20s     | 70  | 80  | 90  |
| ASA        | 20s           | 10  | 20  | 45  |
| AHT        | 240s          | 120 | —   | 360 |
| FTE        | 12            | 8   | —   | 16  |
| Occupancy  | 78%           | 60  | —   | 88  |

Editing an existing hotline seeds the matrix from the stored row instead, so
a null stays null until you type into the cell.

## Field limits and validation

Enforced by the form before it calls the API:

- `code` — uppercased as you type, and runs of whitespace are replaced with
  `-`. Capped at 40 characters by the input. Required: the form trims the
  value first, so a whitespace-only code is rejected with *"Code is
  required"* rather than being submitted.
- `name` — capped at 120 characters by the input. Required, also trimmed
  before the check (*"Name is required"*).
- `hotline_type` — constrained to the six enum values by the dropdown.
- `active` — editable only in edit mode. The create call does not send it.
- KPI fields — numeric or null. No ordering or range check is performed
  client-side.

Anything the server rejects surfaces twice: as a red banner at the top of
the drawer, and as a toast (*"Create failed"* / *"Save failed"*). The
drawer stays open with your input intact so you can correct and resubmit.

## Admin procedures behind this screen

| Procedure                | Input                                                              | Returns                                         |
| ------------------------ | ------------------------------------------------------------------ | ----------------------------------------------- |
| `adminHotlines.list`     | `{ orgId, search?, includeInactive, page, perPage }`               | `{ rows, total }` — identity + target fields    |
| `adminHotlines.get`      | `{ orgId, id }`                                                    | Full row including Min / Avg / Max              |
| `adminHotlines.create`   | `{ orgId, row }` where `row` = `{ code, name, hotline_type, …KPI }` | The created hotline                             |
| `adminHotlines.update`   | `{ orgId, id, patch }` where `patch` adds `active`                  | The updated hotline                             |
| `adminHotlines.remove`   | `{ orgId, id }`                                                    | Deactivates the hotline (see below)             |

`search` matches on code or name and is omitted entirely when the search box
is empty. The console lists 50 hotlines per page and drives the pager from
`total`. `includeInactive` is off by default, so deactivated hotlines
disappear from the list until you tick the box.

Every procedure is org-scoped: `orgId` comes from the active org in the
tenant context, and no call is issued until an org is selected.

## Soft delete and reporting joins

**Deactivate** does not delete the row. It sets `active = false`, which is
what the confirmation dialog means by *"Soft-deleted (active=false). Calls
already routed keep their reporting joins."* The `dim_hotline` row stays in
place so that historical calls which reference it still resolve their
dimension on join — reports over past intervals keep showing the hotline's
code and name.

Practical effects:

- The hotline drops out of the default list. Tick **Include inactive** to
  find it again.
- To bring it back, open it and set **Active** to `Active` in the edit form.
  `adminHotlines.update` carries `active` in its patch, so reactivation is a
  normal save.
- Because the row persists, `code` stays taken. Reactivate rather than
  recreating with the same code.

Deactivating a hotline changes nothing about its stored thresholds. The KPI
values are preserved and come back as they were.

## Tagging hotlines

Each list row has a tag cell backed by the shared tag store under the
resource kind `dim_hotline`. Tags for the whole visible page resolve in a
single call; the cell itself only writes. Clicking a tag cell does not open
the edit drawer.

## Related

- [Telephony Metrics](/glossary/metrics) — definitions and formulas for SLA, ASA, AHT and occupancy
- [Telemetry](/platform/telemetry) — the metric and CDR streams these KPIs are computed from
