# The WorkQueues kit's screens — the rail, the list, an item's head, the team, the settings

> `Osysharp.WorkQueues` ships the screens a team works an inbox from, on the Switchboard floor: its views, queues and saved views as rows of the app's own sidebar with live counts and the "go to" keys; a dense live list worked from the keyboard, with the open item beside it on a wide screen — or, with nothing open, what the view holds and the keys that work it; an item's head with who has it, a hand-to menu that lists only who may hold it, its promises counting down and why it is here; the reader's own status; the team's load for a lead; and six settings pages. Every one reads through the viewer's rules and reflects the app's own policies — no `bool canX` to thread through.

<!-- id: ui-inbox-kit · area: ui · stability: preview · html: https://osysharp.com/reference/ui/inbox-kit/ -->

## Summary        {#summary}
The WorkQueues kit ([The WorkQueues kit — queues, who holds what, and the service levels promised on any item](https://osysharp.com/reference/workflow/inbox-kit/)) keeps where work waits, who holds it and what it was promised. Its screens are
ordinary kit components an app places on its own pages:

```osy syntax
InboxShell(chrome) { Outlet(retain: 4); slot actions { MyStatus(); } }      // the app's sidebar, with the inbox's rows in it
InboxPanes(view, r => "/inbox/" + view + "/i/" + r.Id, openId) { … }       // the list, and beside it what is open
InboxOverview(view);                                                        // …or, with nothing open, what the view holds
WorkItemHead(item);  WhyHere(item);                                         // an item: who has it, its promises, its trail
TeamLoad();  QueueSettings();  SlaPolicySettings();                         // the team, and the settings pages
```

## Signature      {#signature}
```osy syntax
component InboxShell(AppChrome chrome, string prefix = "/inbox");          // SidebarShell + the inbox's nav rows, every slot passed on
NavItem[] InboxNav(InboxCounts c, List<Queue> queues, List<InboxView> views, string prefix, bool accounts = false, bool lead = false);
component InboxPanes(string view, Func<WorkRow, string> hrefOf, Guid open, string title = "", Func<Guid, string>? whoFor = null,
                     BulkMenu? more = null, Action<Guid, List<Guid>>? onMore = null, string listW = "440px",
                     bool headerNamesView = false, Action<List<Guid>, string>? onShown = null,
                     List<Guid>? whoMatches = null) { Slot; }
component InboxOverview(string view, string title = "", string noun = "item") { Slot; }  // the slot: the app's own key lines
component KeyLine(string keys, string what, string also = "");
component WorkList(string view, Func<WorkRow, string> hrefOf, Guid open, string title = "", Func<Guid, string>? whoFor = null,
                   BulkMenu? more = null, Action<Guid, List<Guid>>? onMore = null, bool titled = true,
                   Action<List<Guid>, string>? onShown = null, List<Guid>? whoMatches = null);
component WorkItemHead(WorkItem item, ItemOpening? opened = null);   // reference, title, seats, AssignControl, queue, …
component WorkItemView(WorkItem item, ItemOpening? opened = null);   // the head, and under it WorkItemDetail(item)
[Slot] abstract component WorkItemDetail<T>(T item);                 // what a kind of item IS, drawn by whoever owns the kind
[Slot] abstract component WorkBulkAction(string kind, List<Guid> ids, Action<string> done);   // a button on the bulk bar
component AssignControl(WorkItem item, ItemOpening? opened = null);  // the holder chip + hand-to menu (A), Take (T), Release (U)
component ItemClocks(WorkItem item, ItemOpening? opened = null);     // a chip per promise, counting down live
component WhyHere(WorkItem item, ItemOpening? opened = null);        // the trail, in sentences, live
ItemOpening OpenItem(WorkItem item);         // what those parts (and ItemLinks) ask the server as they open — in one call
component ItemSeats(WorkItem item, string activity = "viewing");
component MyStatus();                        // Available · Busy · Offline · Away until… with cover (Mod+Shift+A)
component TeamLoad();                        // a lead's view of the team
component MyAccountsPanel(string prefix = "/inbox");
component QueueSettings();  component SlaPolicySettings();  component BusinessHoursSettings();
component RoutingRuleSettings(string appAction = "The app's action", string appActionHint = "tag:refund");
component SkillSettings();  component SharedViewSettings(string appFilter = "The app's filter", string appFilterHint = "tag:refund");
```

## Description    {#description}

### The rail is the app's own sidebar   {#rail}
`InboxShell` is the floor's `SidebarShell` with the inbox's rows ahead of the app's own nav — the built-in views
(Mine, Offered to me, My accounts, Unassigned, At risk, All open, Waiting, Done, and Team for a lead), the queues, and the
saved views — so they share the shell's glyphs, current-row marking, collapse and phone drawer. The counts are ONE grouped
read of the items as the reader may read them, live: a new item ticks "Unassigned" without a reload.

The accent is spent on what is yours and live: "Mine" and "Offered to me" wear a breathing lamp while they hold anything,
"Unassigned" counts in the accent, "At risk" in amber, and a queue's count turns amber only while it holds at-risk work.
An app on another shell builds the same rows with `InboxNav`.

Routes are `prefix` + a word: `mine`, `offered`, `accounts`, `unassigned`, `at-risk`, `open`, `waiting`, `done`, a queue
`q-<id>`, a saved view `v-<id>`, one organisation's work `o-<id>`. `WorkList` takes the same word.

### How the list reads, and how it is worked   {#list}
Two lines per item: its lamp and title with the countdown; then its reference, who it is for, the tier, the level, the
priority (coloured only when High or Urgent) and who has it — or, while it is offered first, "→ Ines 0:15" counting down
to when the offer falls through. The **lamp** says what the item waits on (breathing accent new, lit accent owed, ink
answered, a violet ring waiting, red a promise already missed); the **countdown** says how long is left (ink, amber in its
last quarter, red once past — "−0:30"). Due first puts missed promises at the top.

Keys: J/K move, X picks, Enter opens, T takes; with a pick, the bulk bar takes, hands on (A), moves (M), sets the
priority (P) and escalates (E) them all, and says why for any it could not. A row that leaves the view slides out; one
that just arrived drops into place; nothing moves on load.

### The list and what is open beside it   {#panes}
`InboxPanes` lays the list and the open item out the same way in every app: on a screen at least a tablet wide the list
stands on the left and the slot beside it; narrower, each is a screen of its own — the list while nothing is `open`, the
item once something is — and the list's divider is drawn only while something stands beside it. The slot is the app's:
the open item, or, with nothing open, `InboxOverview(view)` — the view's name, how many items it holds and how many are
late, at risk, held by nobody and waiting (from the rail's own grouped read, live, so the two never disagree), and the
keys that work the list, under "No item selected" — `noun:` is the app's word for one ("ticket"). An app adds its own
key lines as the overview's slot (`KeyLine("Mod+K", "commands")`). An empty view says it is clear, and why.

When the app's page header already names the view, say so — `InboxPanes(…, headerNamesView: true)` — and on a phone,
where the list is the whole screen, the list says only how many items it holds rather than the view's name twice
(`WorkList(…, titled: false)`).

In a layout, put the app's `Outlet` in the slot — it stays mounted on a phone too, so a retained item keeps its draft.

**The list reads a WINDOW, never the desk.** The database filters the view, orders it as the reader chose, counts it
(the number in the header) and hands back the first 50 rows with their own clocks and offers; "Show more" reads the next
50. So a page open costs the same on a desk of a hundred items as on one of a hundred thousand — measured on Harbour,
an open of "All open" read 9,215 rows at 500 tickets before and 840 after. The order and the "due first" rank read two
columns the kit keeps on every item as its promises change (`WorkItem.DueNext`, `WorkItem.MissedAt`).

Two things the kit cannot know, the app hands the list:
- **`whoFor`** — who each item is for, by its id, when the requester has no account (a mail sender is a party, not a
  principal). Read it for the rows the list SHOWS, never for every item: the list tells you which with **`onShown(ids,
  search)`** — an action of yours that keeps the ids (and the filter text) in your own state — and your live read takes
  them: `Ticket.Where(t => shown.Contains(t.Id)).Select(t => new TicketFor { … })`. Write the state only when it
  changed; the list tells you on every change it sees. **`whoMatches`** is the other half: the items you find by who
  they are for, for the current filter text (`Ticket.Where(t => search != "" && t.Customer.DisplayName.ToLower()
  .Contains(search)).Take(200).Select(t => t.Id)`), which the list's filter keeps beside its own matches.
- **`more` + `onMore`** — the app's own change to the picked items, as one more menu on the bulk bar (a helpdesk's
  "Status"): `more` names it and its choices, `onMore(choice, ids)` is an action of the app's that does it and says how it
  went.

### What can another kit add to these screens?   {#slots}
Two things, each a slot ([[Slot] and contributions — a place on a page that other kits, and the app, add to](https://osysharp.com/reference/ui/contributions/)) — so an item that arrives from another kit is an ordinary item in the
same inbox, shown and worked the way its own kit decides.

**The item's detail.** `WorkItemView(item)` draws the head and, under it, `WorkItemDetail(item)`: the contribution for
that item's type, and nothing for a type nobody draws. A page that lays an item out itself draws `WorkItemDetail(item)`
where it wants it.

**A change to many items.** With something picked, the list's bulk bar draws `WorkBulkAction(kind, ids, done)` beside
its own buttons. `kind` is the kind key the picked items share (`WorkItem.Kind`), or `""` when they differ or have
none; a contribution for one kind draws its button only for it. Its action calls a function of its own and then
`done(sentence)` — the list says the sentence under the bar and clears the pick.

```osy syntax
// In a kit that brings its own kind of item (`entity Proposal : WorkItem { … }`):
[Contribution("Proposal")]
component ProposalDetail(Proposal item) : WorkItemDetail<Proposal> {
  render { Stack(gap: 2, px: 5, py: 4, label: "Proposal") { SectionLabel("Proposed change"); Text(item.Change); } }
}

[Contribution("Approve all")]
component ApproveProposals(string kind, List<Guid> ids, Action<string> done) : WorkBulkAction {
  action Approve() { var n = ApproveAll(ids); done("Approved " + n + " of " + ids.Count + "."); }
  render { if (kind == "proposal") { Button("Approve all", onPress: Approve, size: Size.Sm, tone: Tone.Primary); } }
}

/// As the person pressing: the rows they may not change are not changed, and are not counted.
public int ApproveAll(List<Guid> ids) {
  return Proposal.Where(p => ids.Contains(p.Id) && !p.Approved).Update(p => p.Approved = true);
}
```

The bulk function is ordinary code: it runs as the person who pressed, so what it changes is what that person may
change — picking an item gives nobody a right over it. In a test, open the item and assert what the detail draws; pick
rows with `Ui.Click("Select <title>")` and press the contribution's button.

### What an item's head shows   {#item}
`WorkItemHead` draws what the inbox knows about an item; the app draws what the item IS below it. Its `AssignControl`
lists **only people who may hold the item** — its queue's members and, where the queue allows, its organisation's
account team, never anybody missing a skill it requires — each with their load against capacity, languages, and whether
they are on. `ItemClocks` counts each promise down from its clock's own due instant, so business hours are exact without
asking the server. `WhyHere` reads the trail: "Routed by 'Billing words' to Billing · offered to Ines (account owner) ·
fell through after 15 minutes · given to Rahul — the least loaded".

### A page that shows several parts of one item asks once   {#opening}
Each of these parts asks the server for what only the server can say — whether the reader may move a colleague's work,
the app's words for its promises, the trail and the link kinds in the reader's language. On its own each asks as it
opens. A page that shows several of them on one item asks ONCE, with `OpenItem(item)`, and hands the answer to each as
`opened:` — so opening the item, or moving from one to the next, is one call to the server instead of one per part.
`WorkItemHead` does this for its own parts. A part handed an opening that has not arrived yet waits for it rather than
asking for itself.

```osy syntax
ItemOpening opened = new ItemOpening();
on mount { opened = OpenItem(item); }
render {
  AssignControl(item, opened: opened);
  ItemClocks(item, opened: opened);
  WhyHere(item, opened: opened);
  ItemLinks(item, other => "/work/" + other.Id, opened: opened);
}
```

### The reader, the team, the settings   {#people}
`MyStatus` is what routing reads — never whether a tab is open. `TeamLoad` (a lead's) shows each person's load against
capacity, what is at risk, who is away and who covers, and marks a person away or back. The six settings pages edit the
inbox admins' rows in the floor's settings area (Save, Discard, Undo); anybody else sees them locked, with the reason.
`SlaPolicySettings` answers "which policy applies to an item like this?" before anything is saved (`WhichPolicy`).

### Reflecting the app's policies   {#policies}
The kit's components name `WorksInbox`, `LeadsInbox` and `AdministersInbox` directly — `live var lead = LeadsInbox;` — and
the app's definitions answer, because a package component that reflects an app-supplied policy is compiled with the app.
Nothing is threaded through as a `bool canX`; the server enforces every verb whatever a screen shows ([The WorkQueues kit's floor — restricted queues, who may hold an item, and who moves whose work](https://osysharp.com/reference/security/inbox-kit/)).

## Examples       {#examples}

### A desk on the kit's screens   {#example-desk}
```osy title="the rail, a list, an item beside it, and the team" test app=ui-inbox-kit
app Desk {
  use Osysharp.Identity@0;
  use Osysharp.Organizations@0;
  use Osysharp.Organizations.ServicePlans@0;
  use Osysharp.Presence@0;
  use Osysharp.Observability;
  use Osysharp.WorkQueues@1;
  use Osysharp.Workflow;
  use Osysharp.ServiceHours;
  use Osysharp.Ui;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.WorkQueues;

[Role] enum DeskRole { Authenticator, Agent, Lead, Admin }
policy WorksInbox => RoleGrant.Any(g => g.Member == user
  && (g.Role == DeskRole.Agent || g.Role == DeskRole.Lead || g.Role == DeskRole.Admin));
policy LeadsInbox => RoleGrant.Any(g => g.Member == user && (g.Role == DeskRole.Lead || g.Role == DeskRole.Admin));
policy AdministersInbox => RoleGrant.Any(g => g.Member == user && g.Role == DeskRole.Admin);
policy ManagesOrganizations => AdministersInbox;
policy CollaboratesOnRecords => WorksInbox;

entity Ticket : WorkItem {
  [MaxLength(4000)] string Body = "";
  security { allow read, create, update when WorksInbox; }
}

[Layout]
component Frame() {
  render {
    InboxShell(new AppChrome { Product = "Desk", Mark = Icons.Inbox, Home = "/inbox/mine" }) {
      Outlet(retain: 4);
      slot actions { MyStatus(); }
    }
  }
}

[Route("/inbox/{view}")] [Layout(Frame)] [Authorize(WorksInbox)] [Render(CSR)]
component Work(string view) {
  render {
    InboxPanes(view, r => "/inbox/" + view + "/i/" + r.Id, Guid.Empty) {
      InboxOverview(view) { KeyLine("Mod+K", "go anywhere"); }
    }
  }
}

[Route("/inbox/{view}/i/{id}")] [Layout(Frame)] [Authorize(WorksInbox)] [Render(CSR)]
component OneTicket(string view, Guid id) {
  live var tickets = Ticket.Where(t => t.Id == id).Include(t => t.Queue).Include(t => t.Assignee).ToList();
  render {
    InboxPanes(view, r => "/inbox/" + view + "/i/" + r.Id, id) {
      Stack(grow: 1, minW: "0", overflowY: Overflow.Auto) {
        foreach (var t in tickets) {
          WorkItemHead(t);
          Box(p: 5) { Text(t.Body); }
          Box(px: 5) { WhyHere(t); }
        }
      }
    }
  }
}

[Route("/inbox/team")] [Layout(Frame)] [Authorize(LeadsInbox)] [Render(CSR)]
component Team() { render { Box(p: 5) { TeamLoad(); } } }
```

## See also       {#see-also}
- [The WorkQueues kit — queues, who holds what, and the service levels promised on any item](https://osysharp.com/reference/workflow/inbox-kit/) — what the screens read: queues, holders, promises, routing, away and escalation
- [The WorkQueues kit's floor — restricted queues, who may hold an item, and who moves whose work](https://osysharp.com/reference/security/inbox-kit/) — the floor every screen stands on
- [Menus, the command menu, and keyboard shortcuts](https://osysharp.com/reference/ui/menus/) — `shortcut:`, key sequences ("G then M") and the `?` sheet
- [SettingsArea — an app's settings: sections, explicit save and discard, undo, who changed what](https://osysharp.com/reference/ui/settings/) — the settings area the six pages sit in
- [The Presence kit — who else is on a record, and who moved it first](https://osysharp.com/reference/realtime/collaboration-kit/) — the seats `ItemSeats` draws
- **`kits/work-queues/tests/ui.test.osy`** — every screen driven as a team drives it, photographed under `--pixels`
