# Sites

> The dim_site record: code, timezone, country and region, the field limits the form enforces, and what deactivation does.

A **site** is a physical location in your operation — a floor, a
building, a BPO campus. In TeleQuick it is a row in the `dim_site`
dimension table, managed from **Admin → Sites** and backed end-to-end by
the `adminSites.*` tRPC procedures (`list`, `create`, `update`,
`remove`).

Sites exist so that other screens have a stable thing to group and
filter by. The Sites screen states the consumers directly: the
wallboard, the adherence calendars, and the Agent directory all filter
by site.

## The site record

Every site row carries these fields:

| Field      | Type              | Required | Limit enforced by the form | Notes |
| ---------- | ----------------- | -------- | -------------------------- | ----- |
| `id`       | string            | assigned | —                          | Server-assigned. Shown in the edit drawer header as `id <value>`. |
| `code`     | string            | yes      | 40 characters              | Upper-cased as you type. |
| `name`     | string            | yes      | 120 characters             | Free text, e.g. `Manila Eastwood`. |
| `timezone` | IANA timezone     | yes      | picked from a list         | Defaults to `UTC` on a new site. |
| `country`  | string or `null`  | no       | 2 characters               | ISO 3166-1 alpha-2, upper-cased as you type. |
| `region`   | string or `null`  | no       | 40 characters              | Free text, e.g. `APAC`. |
| `active`   | boolean           | —        | —                          | Editable only when editing an existing site. |

`country` and `region` are trimmed on save, and a blank value is stored
as `null` rather than an empty string. `code` and `name` are trimmed
before validation, so a whitespace-only value is rejected with
`Code is required` / `Name is required` even though the browser's
`required` attribute would have accepted it.

## Code

The code is the short handle for the site — `MNL-1`, `AUS-3`. The form
upper-cases every keystroke, so codes are always stored in upper case,
and it caps the field at 40 characters.

In the list, the code renders as a chip in the leftmost column, and it
is the value the deactivation dialog uses to identify the site
(`Deactivate site MNL-1?`). Treat it as the human-readable key you will
see quoted back to you elsewhere in the console, and keep it short
enough to fit in a table cell.

## Timezone

Timezone is a single IANA zone name per site, chosen from a combobox
rather than typed. The option list comes from the browser itself, via
`Intl.supportedValuesOf('timeZone')`, so it is the full IANA set the
runtime ships. If the runtime does not expose that helper, the form
falls back to a short curated list:

`UTC`, `America/New_York`, `America/Los_Angeles`, `America/Mexico_City`,
`America/Sao_Paulo`, `Europe/London`, `Europe/Paris`, `Europe/Berlin`,
`Asia/Kolkata`, `Asia/Singapore`, `Asia/Manila`, `Asia/Tokyo`,
`Australia/Sydney`.

A new site starts at `UTC`. Set it to the real local zone of the floor
before anyone starts scheduling against the site — the value is stored
verbatim on the row and shown in the list in monospace.

## Country and region

Both fields are optional, and both exist as reporting groupings rather
than as behaviour switches.

- **Country** is constrained to two characters and upper-cased, i.e. an
  ISO 3166-1 alpha-2 code such as `PH` or `US`. Keeping to the ISO code
  rather than a spelled-out name means sites from different teams group
  together cleanly.
- **Region** is free text up to 40 characters — `APAC`, `EMEA`,
  `LATAM`. Because it is free text, consistency is on you: `APAC` and
  `apac` are two different groupings.

Both are also matched by the list's search box, so a well-populated
country and region make sites findable without knowing the code.

## Creating and editing a site

**New site** opens a 520px drawer with an empty form. Clicking any row
in the table opens the same drawer pre-filled for that site. The
differences between the two modes:

| | Create | Edit |
| --- | --- | --- |
| Procedure | `adminSites.create({ orgId, row })` | `adminSites.update({ orgId, id, patch })` |
| `active` field | not shown, not sent | shown as an **Active** checkbox and sent in the patch |
| Footer | Cancel / Create | Deactivate / Cancel / Save |
| Success toast | `Site created` | `Saved` |

Both mutations are scoped to the organisation currently selected in the
tenant switcher; `orgId` comes from the active org and is sent on every
call, including `list`.

On success, the screen invalidates the `adminSites.list` cache and
closes the drawer, so the table reflects the change immediately. On
failure the error message is rendered inline at the top of the form
*and* raised as an error toast (`Create failed` / `Save failed`); the
drawer stays open with your input intact.

## Searching and paging the list

The filter bar has two controls:

- **Search** — a single box that matches against code, name, country and
  region. It is passed through as `search` on `adminSites.list`, and is
  omitted entirely when blank. Typing resets you to page 1.
- **Include inactive** — a checkbox mapped to `includeInactive`. Unticked
  (the default) the list shows active sites only; tick it to see
  deactivated sites alongside them. Toggling it also resets to page 1.

The list is paged at 50 rows per request (`perPage`), and the pager is
sized from the `total` the procedure returns. That same `total` drives
the count in the page header (`12 sites`). Previous results stay on
screen while the next page loads, so paging does not flash an empty
table.

When nothing matches, the screen distinguishes the two empty cases: with
a search term it says nothing matched and offers no action; with no
search term it offers a **Create first site** button.

## Deactivation is a soft delete

There is no hard delete. The **Deactivate** button in the edit drawer
calls `adminSites.remove({ orgId, id })`, which sets `active = false` on
the row. The confirmation dialog spells out why:

> Sites are soft-deleted (`active=false`) so reports keep joining
> cleanly. You can re-activate later.

What changes when you deactivate:

- The row's status chip flips from **Active** to **Inactive**.
- The site drops out of the default list view — you need **Include
  inactive** ticked to see it again.

What does not change:

- The row is still there. Its `id`, `code`, `name`, `timezone`,
  `country`, `region` and tags are untouched, so anything that joins to
  `dim_site` by id still resolves.

To bring a site back, tick **Include inactive**, open the site, tick
**Active** in the edit form and save. Re-activation is an ordinary
`update` — no special procedure, and nothing is recreated.

## Tagging sites

The rightmost column of the table is a tag cell. Tags on this screen use
the resource kind `dim_site` and are scoped to the active org.

Tags for the whole visible page are resolved in a single call when the
page loads, keyed by site id; the cell itself only writes. Editing tags
in a row does not open the site drawer — the cell stops the click from
propagating — and after a change the screen refreshes the tag state for
the page.

Because tags live alongside the row rather than on it, they survive
deactivation.

## Related

- [Authentication](/concepts/authentication) — how the console's calls are scoped to an org
