# The Client Area

https://dev.wisecp.com/es/the-client-area

Twenty five views share one shell that a single controller flag turns on. Every one can appear for an account other than the person logged in.

## Overview

This is the only family where the same page answers two questions: what is on it, and whose it is. A member can act on another account, so a list may belong to someone else. A tab may be missing because a permission was withheld, not because the data is empty.

The views extend the public layout, and a flag turns it into the client shell.

## Structure

The twenty five views group into six jobs.

| Group | Views | Notes |
| --- | --- | --- |
| Dashboard | `dashboard-hero`, `dashboard-standard` | Two variants of one address, or a single `dashboard` view |
| Assets | `services`, `service-detail`, `domains`, `domain-detail`, `service-transfer-approve`, `license-transfer-verify` | Lists use a preset; detail pages are multi tab and deep linkable |
| Money | `invoices`, `invoice-detail`, `subscriptions`, `bulk-pay`, `balance` | Two extend the invoice layout |
| Support | `tickets`, `ticket-detail`, `ticket-create`, `ticket-msg` | `ticket-msg` is a fragment: one reply for the poller |
| Account | `settings`, `sub-accounts`, `api-credentials`, `sms` | Avatar menu pages; they mark no tab |
| Programs | `affiliate`, `reseller`, `reseller-program`, `access-denied` | One reseller address, two views |

## Step by Step

### Turn On the Client Shell

1. Compute the flag once at the top of the layout: signed in *and* sub navigation requested.
2. Use it to choose the chrome: client topbar, sidebar and footer, no marketing bands.
3. Mark the active tab from `$subnav_active`. Controllers fix its values: `dashboard`, `services`, `domains`, `billing`, `support`, `sms`. Avatar menu pages carry an empty string.
4. Do not invent tab names: an unrecognised value shows no active tab, silently.

### Build for the Right Account

1. Never print the login identity as the owner: one is who signed in, the other is whose data is on screen.
2. Expect a panel to be absent rather than empty; a withheld permission means the query never ran.
3. Keep the account switcher visible; a member acting for another needs a way back.

### Build a List Surface

The four big lists print rows twice: on the server for the first page, through the table engine after.

1. Put the row markup in the theme's table preset, under `tables/`.
2. Have the view's loop print the same markup, so first paint and an AJAX page are byte identical.
3. Read page values from the table options, not globals: the preset sees no controller data.

### Make Detail Tabs Linkable

1. Give each tab button a stable key attribute, separate from the pane id, so the page does not jump.
2. On load, open the tab named in the query string; it must override the remembered tab.
3. When a tab is shown, write its key back and drop parameters from the tab you left.
4. For a lazily loaded tab, check on load and again on the next tick: the remembered tab is restored at two moments.

## Reference

### Shell Variables

- **$show_client_subnav**: Turns the default layout into the client shell. Set true **before** the predefined client data.
- **$subnav_active**: The current sub navigation tab; empty string is deliberate.
- **$notification_count**: The bell badge, always set even at zero, with a preformatted `$notification_count_text`. Core chrome.
- **$account_info**: The identity block, always the **signed-in member**. Keys: `name`, `surname`, `full_name`, `email`, `avatar`, `initials`, `balance` (formatted), `support_pin`, `two_factor`, `is_reseller`, `dealership`, `last_login`, `last_login_date`, `last_login_ip`, `last_login_country`, `last_login_city`. Fall back avatar → initials → icon.
- **$account_info.last_login_date**: Empty when the account has never signed in. Formatting an absent date stamps the current time and presents now as the last sign in — print a dash.
- **$currency_formats_json and siblings**: Sample strings, currency ids and rates, so the money script matches the server.

### The Account Context API

```php
// Null when there is no member session. Never mutates the login identity.
public static function activeAccount(): ?array;

// Own account is always allowed; a switched account only when the permission was granted.
public static function accountCan(string $permission): bool;

// Only a still-valid membership (or self) is accepted; a client-supplied id is never trusted.
public static function switchAccount(int $ownerId): bool;

// The bell count for the signed-in member. 0 means "no member" as well as "nothing unread".
public static function notification_count(int $user_id = 0): int;
```

```php
$ctx = [
    'login_id'    => 41,      // who authenticated; NEVER changes while switched
    'owner_id'    => 88,      // whose data this page shows
    'is_self'     => false,   // true when the two ids are the same
    'permissions' => ['view_services', 'view_invoices'],   // re-read every request
];

// The switched account is re-validated against the membership table on EVERY request,
// so a revoked membership loses access immediately rather than at the next login.
```

### Dashboard Data

| Variable | Holds | Behaviour when unpermitted |
| --- | --- | --- |
| `$dash_services`, `$dash_domains`, `$dash_invoices`, `$dash_tickets` | The panel lists | Empty; the query never runs |
| `$dash_stats` | The figure strip | Built from permitted reads |
| `$dash_alerts` | Attention items, through a filter hook modules extend | Fewer entries, never missing |
| `$dash_health`, `$dash_feed`, `$dash_balance` | Health line, feed, balance | Present and empty — "not yours to see", not zero |
| `$dash_greeting`, `$dash_first_name`, `$dash_today` | The welcome line; names the signed-in member | Always present |
| `$dash_l10n` | Strings the dashboard scripts label with | Always present |
| `$show_activity` | Whether the activity feed may appear | False when switched: activity is personal |

### Client Area Theme Flags

- **dashboard_layout**: A settings field, `hero` or `standard`, selecting `views/account/dashboard-{value}`. Ignored when a single `account/dashboard` ships.
- **meta.dashboard_due_soon_alert**: A manifest flag, not a setting. False alerts only on overdue invoices; true also on the next.
- **meta.disabled_routes**: Route keys your theme does not serve, answered with 404 rather than a half finished inherited view. Templates see the list too.

## Example

```smarty
{* Computed ONCE, at the very top, before the doctype. Both halves are required:
   a signed-in visitor on a marketing page must still get the public chrome. *}
{$is_client_area = $is_logged_in && !empty($show_client_subnav)}<!DOCTYPE html>
<html lang="{$ui_lang|default:'en'}" dir="{$ui_dir|default:'ltr'}">
<head>
    {* Shell-only stylesheet: a public page never pays for it. *}
    {if $is_client_area}<link rel="stylesheet" href="{asset path='css/client-nav.css'}">{/if}
    {block name=head}{/block}
    {hook name='ui:client.head.css'}
</head>

<body class="{block name=body_class}{/block}"{if $is_client_area} data-client-nav="sidebar"{/if}>
{hook name='ui:client.body.begin'}

{if $is_client_area}
    {include file='partials/client-topbar.tpl'}
    {include file='partials/client-sidebar.tpl'}
{else}
    {include file='partials/header.tpl'}
{/if}

<main class="client-main">
{block name=content}{/block}
</main>

{* Marketing bands belong to the public site only. *}
{if !$is_client_area}{block name=bands}{/block}{/if}

{if $is_client_area}
    {include file='partials/client-footer.tpl'}
{else}
    {include file='partials/footer.tpl'}
{/if}

{block name=body_end}{/block}
{hook name='ui:client.body.end'}
```

```php
// The identity is never the owner. Resolve both, then ask about permissions.
$ctx  = UserManager::activeAccount();
$uid  = (int) $ctx["owner_id"];
$self = (bool) $ctx["is_self"];

// Own account bypasses the gate entirely; a switched account is checked per area.
$can = fn (string $p): bool => $self || in_array($p, $ctx["permissions"], true);

if (!Theme::active()->viewExists("account/services")) return $this->page_404("website");

// MUST come before set_predefined_data("client"): the notification count is read
// behind this flag, and a controller that sets it afterwards gets a zero badge.
$this->addData("show_client_subnav", true);
$this->addData("subnav_active", "services");

// An unpermitted area contributes nothing rather than an empty-looking panel.
$this->addData("service_list", $can("view_services") ? $this->model->services($uid) : []);

$this->set_predefined_data("client");

return $this->view->chose("website")->render("account/services", $this->data, true);
```

## Pitfalls

> **Set the shell flag before the client data, not after**
> 
> The notification count is computed behind `show_client_subnav`. Set the flag afterwards and the badge is always zero.

> **Hiding a link is not access control**
> 
> Omitting a tab for an unpermitted area is presentation. The boundary is the guard behind the access denied view, which runs regardless.

> **A table preset may be included twice**
> 
> Declare its helpers as variables holding closures: a named function fatals on the second include, which an AJAX page request returning a summary produces.

> **Set globals unconditionally, hide them conditionally**
> 
> A counter the shell reads is always assigned, even at zero. A variable set inside an `if` is undefined wherever the branch is skipped.

> **Localise the cancel button too**
> 
> The confirmation dialog defaults its cancel label to English, so passing only the action label leaves a stray English word in other languages.

## Related Articles

- [Page Surfaces](https://dev.wisecp.com/en/page-surfaces)
- [Login and Registration](https://dev.wisecp.com/en/login-and-registration)
- [Template Variables](https://dev.wisecp.com/en/template-variables)
- [Theme Hooks and Output Filters](https://dev.wisecp.com/en/theme-hooks-and-output-filters)
- [Interface Components](https://dev.wisecp.com/en/interface-components)
