The Client Area

2 views Markdown

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.

GroupViewsNotes
Dashboarddashboard-hero, dashboard-standardTwo variants of one address, or a single dashboard view
Assetsservices, service-detail, domains, domain-detail, service-transfer-approve, license-transfer-verifyLists use a preset; detail pages are multi tab and deep linkable
Moneyinvoices, invoice-detail, subscriptions, bulk-pay, balanceTwo extend the invoice layout
Supporttickets, ticket-detail, ticket-create, ticket-msgticket-msg is a fragment: one reply for the poller
Accountsettings, sub-accounts, api-credentials, smsAvatar menu pages; they mark no tab
Programsaffiliate, reseller, reseller-program, access-deniedOne 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

exact signatures
// 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;
what activeAccount returns
$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

VariableHoldsBehaviour when unpermitted
$dash_services, $dash_domains, $dash_invoices, $dash_ticketsThe panel listsEmpty; the query never runs
$dash_statsThe figure stripBuilt from permitted reads
$dash_alertsAttention items, through a filter hook modules extendFewer entries, never missing
$dash_health, $dash_feed, $dash_balanceHealth line, feed, balancePresent and empty — "not yours to see", not zero
$dash_greeting, $dash_first_name, $dash_todayThe welcome line; names the signed-in memberAlways present
$dash_l10nStrings the dashboard scripts label withAlways present
$show_activityWhether the activity feed may appearFalse 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

layouts/default.tpl, the client branch
{* 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'}
the controller half, with the permission gate
// 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.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.