# DetailView Page

For UI component API details, see [components.md](components.md).

## File Structure

```bash
[id]/
  page.tsx                          # route entry — fetches data, composes fragments
  components/
    <entity>-detail.tsx             # DescriptionCard with field definitions
    <entity>-actions.tsx            # ActionPanel (when actions exist)
```

## Page Pattern

```tsx
import { useQuery } from "urql";
import {
  Layout,
  Button,
  Link,
  useParams,
  useOverrideBreadcrumb,
} from "@tailor-platform/app-shell";
import { Loading } from "@/components/composed/loading";
import { ErrorFallback } from "@/components/composed/error-fallback";

const EntityPage = () => {
  const { id } = useParams();
  const [{ data, error, fetching }, reexecuteQuery] = useQuery({
    query: EntityQuery,
    variables: { id: id! },
  });

  useOverrideBreadcrumb(data?.entity?.name); // data-driven breadcrumb

  if (fetching) return <Loading />;
  if (error || !data?.entity)
    return (
      <ErrorFallback
        title="Failed to load"
        message="An error occurred."
        onReset={() => reexecuteQuery({ requestPolicy: "network-only" })}
      />
    );

  return (
    <Layout>
      <Layout.Header 
        title="Entity Detail" 
        actions={<Button render={<Link to="edit" />}>Edit</Button>} 
      />
      <Layout.Column><EntityDetail entity={data.entity} /></Layout.Column>
      {/* Right column — see "Right Column Composition" below */}
      <Layout.Column>
        <EntityActions entity={data.entity} />
        <ActivityCard items={activities} title="Activity" />
      </Layout.Column>
    </Layout>
  );
};
```

- `<entity>-detail.tsx` — exports fragment + component using `DescriptionCard`
- `<entity>-actions.tsx` — exports fragment + component using `ActionPanel` (only when actions exist)
- `useOverrideBreadcrumb` — shows entity name instead of raw `:id`. Shows default while `undefined` (loading). Cleaned up on unmount.
- `activities` — fetched alongside the entity query or from a separate activity/audit-log query, depending on the backend schema.

> Component API references: [Layout](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/layout.md) · [Button](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/button.md)
>
> `Button` supports a `render` prop for polymorphic rendering — e.g. `<Button render={<Link to="edit" />}>` renders as a link with button styling.

### Right Column Composition

The right `Layout.Column` holds `ActionPanel` and/or `ActivityCard`. Include each only when the screen spec warrants it (see their "When to Use" sections below). The possible combinations:

| Combination | When | Example |
| --- | --- | --- |
| **ActionPanel + ActivityCard** | Actions exist and change tracking matters | Sales order with approve/ship actions and status history |
| **ActionPanel only** | Actions exist but no meaningful change history | Product detail with an "Edit" action |
| **ActivityCard only** | No direct actions but change tracking matters | Completed invoice viewed for audit purposes |
| **Neither (no right column)** | Read-only view with no actions or history | Static reference data like a country or currency record |

When neither component is needed, omit the second `Layout.Column` entirely.

### Layout.Header actions vs ActionPanel

`Layout.Header` accepts an `actions` prop for buttons in the page header. It serves a different role from `ActionPanel`:

| | `Layout.Header` actions | `ActionPanel` |
| --- | --- | --- |
| **Position** | Top-right of the page header, always visible | Right column card, scrolls with content |
| **Rendering** | `Button` components (primary / secondary) | Icon + label list rows |
| **Purpose** | **Primary page-level actions** — the main thing the user came to do on this page | **Contextual entity operations** — secondary or related actions |
| **Examples** | Edit, Save, Cancel, Back | Approve, Create invoice, Send email, Delete |
| **Cardinality** | 1–2 buttons | Multiple items |

Rule of thumb: if an action is **the primary reason the user visits this page**, put it in `Layout.Header` actions (e.g. "Edit" button). If it is a **secondary or contextual operation** available from this page, put it in `ActionPanel`.

## DescriptionCard

> Full API reference: https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/description-card.md

Renders a key/value grid for entity detail display. The `data` prop accepts a nested object — dot-notated keys like `customer.name` are resolved internally.

When an entity has logically distinct sections, stack multiple `DescriptionCard` components.

```tsx
import { DescriptionCard } from "@tailor-platform/app-shell";

<DescriptionCard
  title="Order Details"
  columns={3}
  data={data.order}
  fields={[
    { key: "orderNumber", label: "Order #", type: "text", meta: { copyable: true } },
    { key: "status", label: "Status", type: "badge", meta: { badgeVariantMap: { ACTIVE: "success", DRAFT: "neutral" } } },
    { key: "totalAmount", label: "Total", type: "money", meta: { currencyKey: "currency" } },
    { key: "createdAt", label: "Created", type: "date", meta: { dateFormat: "medium" } },
    { type: "divider" },
    { key: "customer.name", label: "Customer", type: "reference", meta: { referenceIdKey: "customer.id", referenceUrlPattern: "/customers/{id}" } },
  ]}
/>
```

## ActionPanel

> Full API reference: https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/action-panel.md

Vertical list of actions for entity operations. Renders icon + label rows in a card in the right column.

### When to Use

Use `ActionPanel` when the DetailView screen spec defines **contextual operations related to the entity** — things the user does *from* or *about* the resource. Typical actions:

- **State transitions** — approve, reject, cancel, archive, activate/deactivate
- **Related resource creation** — "Create invoice from this order", "Create delivery note"
- **Destructive operations** — delete, void (usually with confirmation dialog)
- **External triggers** — send email, sync to external system, generate PDF
- **Navigation to related resources** — "View linked purchase order"

If the DetailView has no contextual operations (e.g. a completed record viewed for reference), omit `ActionPanel`.

### Error Handling in Actions

The scaffold provides a local `useToast` wrapper at `@/hooks/use-toast` that enhances app-shell's base `useToast` with `CombinedError` support. An ESLint rule enforces using this wrapper instead of importing `useToast` directly from `@tailor-platform/app-shell`.

```tsx
import { useToast } from "@/hooks/use-toast";
import { useMutation } from "urql";
import { Trash2 } from "lucide-react";

const toast = useToast();
const [, executeMutation] = useMutation(DeleteEntityMutation);

const actions = [
  {
    key: "delete",
    label: "Delete",
    icon: <Trash2 />,
    onClick: () =>
      executeMutation({ id }).then((result) => {
        if (result.error) {
          toast.error(result.error); // accepts CombinedError — extracts human-readable message automatically
          return;
        }
        toast.success("Deleted");
      }),
  },
];
```

### Actions with Required Input

When an action requires additional user input before calling the mutation, use a Dialog to collect it.

1. Use `useState<string | null>(null)` to track which dialog is open
2. The ActionPanel item sets the dialog key instead of calling the mutation directly
3. The dialog collects the required inputs and calls the mutation on confirm

```tsx
const [openDialog, setOpenDialog] = useState<string | null>(null);

// In ActionPanel actions:
{
  key: "close",
  label: "Close Order",
  icon: <XCircle />,
  onClick: () => setOpenDialog("close"),
}

// Dialog:
<Dialog.Root open={openDialog === "close"} onOpenChange={() => setOpenDialog(null)}>
  <Dialog.Content title="Close Purchase Order">
    <Checkbox checked={writeOff} onCheckedChange={setWriteOff}>
      Write off remaining quantity
    </Checkbox>
    {writeOff && (
      <Field.Root>
        <Field.Label>Reason</Field.Label>
        <Field.Control>
          <Input value={reason} onChange={(e) => setReason(e.target.value)} />
        </Field.Control>
      </Field.Root>
    )}
    <Dialog.Footer>
      <Button onClick={handleClose}>Confirm</Button>
    </Dialog.Footer>
  </Dialog.Content>
</Dialog.Root>
```

## ActivityCard

> Full API reference: https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/activity-card.md

Timeline of recent activities on a resource. Place in the right `Layout.Column`, below `ActionPanel` when both are present.

### When to Use

Use `ActivityCard` when **tracking who changed what and when** matters for the resource. Typical indicators:

| Indicator | Example |
| --- | --- |
| **Multi-actor editing** — multiple users create or update the same resource | Purchase orders edited by both buyers and warehouse staff |
| **Approval / review workflow** — the resource goes through explicit approval steps | Expense reports requiring manager approval before finance processing |
| **Status lifecycle tracking** — the resource has states whose transitions need an audit trail | Sales orders moving through Draft → Confirmed → Shipped → Delivered |
| **Compliance / audit requirements** — regulatory or internal policy demands a change log | Inventory adjustments that must record who adjusted, when, and why |

If the resource is simple CRUD with a single owner and no meaningful state transitions (e.g. a user profile settings page), `ActivityCard` is unnecessary.

### Standalone API

```tsx
import { ActivityCard } from "@tailor-platform/app-shell";

<ActivityCard
  items={[
    { id: "1", actor: { name: "Hanna" }, description: "confirmed the order", timestamp: new Date() },
    { id: "2", description: "Status changed to EXPIRED", timestamp: new Date() }, // system event
  ]}
  title="Updates"
  maxVisible={6}      // overflow shows "N more activities" button → dialog
  groupBy="day"       // "none" | "day" — groups under TODAY / YESTERDAY / date labels
/>
```

### Compound API

For custom rendering (icons, badges, mixed item types), use `ActivityCard.Root` → `ActivityCard.Items` → `ActivityCard.Item`. Items must extend `ActivityCardBaseItem` (`id` + `timestamp`). See the [full API reference](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/activity-card.md) for all props.

```tsx
import { ActivityCard, type ActivityCardBaseItem } from "@tailor-platform/app-shell";

interface MyItem extends ActivityCardBaseItem {
  kind: "approval" | "update";
  label?: string;
  message?: string;
}

const items: MyItem[] = [
  { id: "1", timestamp: new Date(), kind: "approval", label: "PO approved" },
  { id: "2", timestamp: new Date(), kind: "update", message: "Status changed to CONFIRMED" },
];

<ActivityCard.Root items={items} title="Updates" groupBy="day">
  <ActivityCard.Items<MyItem>>
    {(item) =>
      item.kind === "approval" ? (
        <ActivityCard.Item indicator={<ApprovedIcon />}>
          <p>{item.label}</p>
        </ActivityCard.Item>
      ) : (
        <ActivityCard.Item>
          <p>{item.message}</p>
        </ActivityCard.Item>
      )
    }
  </ActivityCard.Items>
</ActivityCard.Root>;
```
