Application Shell

Every authenticated route renders inside BaseComponent (src/app/views/theme/base/). The shell owns the navigation, the page title, the global banners and the session-wide subscriptions — a feature page only supplies the router-outlet content.

Composition

theme/base/

BaseComponent — the Material mat-sidenav-container hosting everything else, plus BaseComponentService.

theme/aside/

AsideLeftComponent — the live primary navigation, and user-details/.

theme/header/

Top bar, plus header-mobile/ for the mobile toggle.

theme/subheader/

Page title, description and breadcrumbs.

theme/footer/

Footer with the build version.

theme/brand/

Logo and white-label branding.

theme/html-class.service.ts

Writes layout CSS classes onto <body>.

partials/layout/splash-screen/

The bootstrap loading screen.

BaseComponent

Two things happen in the constructor, before any lifecycle hook:

constructor(/* … */) {
    super();
    this.baseService.loadConfigs();          (1)
    this.globalSubscriptionsService.init();  (2)
}
1 Loads LayoutConfig, MenuConfig and PageConfig into their services and applies the resulting body classes.
2 Starts every session-scoped background subscription. Idempotent — see Architecture Overview.
The shell is where the session actually starts

GlobalSubscriptionsService.init() is called from BaseComponent, not from AppModule or an APP_INITIALIZER. Anything registered there therefore runs only once the user reaches an authenticated route — never on the login screen and never during the onboarding shell, which does not use BaseComponent.

ngAfterViewInit then performs the post-render work:

clearOldMessagesFromCache()

Drops dashboard-message dismissals older than seven days.

showNewMessages()

Fetches and shows the server-driven announcement banner.

checkIfIOSUserAndFixPaddingIssue()

Applies pyb-ios-padding and a pixel height for iOS Safari.

sidebarFixer()

Re-runs updateContentMargins() on the drawer container.

sidebarFixer() reaches into two private Material fields:

(this.sidenavContainer as any)._ngZone.onMicrotaskEmpty
    .pipe(debounceTime(10))
    .subscribe(() => {
        this.sidenavContainer.updateContentMargins();
        (this.sidenavContainer as any)._changeDetectorRef.markForCheck();
    });

_ngZone and _changeDetectorRef are private Material internals. The cast keeps the drawer content margins correct when the aside is toggled, but it is the first thing to break on a Material upgrade.

Top bar versus banners

The onboarding status bar and the global banners are mutually exclusive:

this.topBarService.details$.subscribe(details => {
    if (!details) {
        this.canBannerBeVisible = true;
        return;
    }
    this.hasTopBar = details.showStatusBar;
    this.canBannerBeVisible = !details.showStatusBar;
    this.cd.detectChanges();
});

A supplier still in onboarding sees the progress bar; an activated supplier sees banners. See Onboarding.

Dashboard announcement banners

BaseComponentService reads DashboardMessageService.getSupplierDashboardMessage() and shows anything whose config_key starts with supplier_dashboard_message, as a BannerTypes.ANNOUNCEMENT banner. Dismissal is recorded in localStorage under the message key, and messageClosedWeekAgo() expires those records after seven days.

One message is deliberately un-dismissable
localStorage.removeItem(this.dashboardMessageKey);

The exact key supplier_dashboard_message (without a suffix) has its dismissal record deleted on every shell init, and is rendered with hideClose: true. It reappears on every page load by design — the code comment cites task CA-744. Suffixed keys such as supplier_dashboard_message_holiday remain dismissable.

Configuration objects

Three plain classes in core/_config/ describe the shell. They are loaded into matching services by BaseComponentService.loadConfigs().

Config Service Describes

LayoutConfig

LayoutConfigService

Skins, fixed/fluid widths, logos, the splash-screen loader, and a JS colour palette.

MenuConfig

MenuConfigService → MenuAsideService

The navigation trees.

PageConfig

PageConfigService → SubheaderService

Per-route title, description and breadcrumb text.

HtmlClassService.setConfig() turns the layout config into <body> classes. Three of them are hardcoded rather than derived:

private initSubheader() {
    document.body.classList.add('kt-subheader--overlaped');
    document.body.classList.add('kt-subheader--primary');
}

private initSkins() {
    document.body.classList.add('pyb-aside-dark');
}

Changing subheader.style or aside.self.skin in LayoutConfig therefore has no effect on the initial render — only SubheaderService.changeStyle() / changeColor(), which swap the classes at runtime, do.

MenuConfig defines two trees, header and aside. Each item is:

{
    title: 'Seller Card',
    root: true,
    alignment: 'left',
    page: URLS_ROUTER.SELLER_CARD.ROOT,
    translate: 'MENU.SELLER_CARD',
    permission: 'SELLER_CARD',
    icon: 'pyb-credit-card',
}

page

Route path, always taken from URLS_ROUTER. Path Aliases

translate

i18n key for the label. title is a developer-facing fallback.

permission

Dotted PermissionsEnum string. Omit for an ungated item.

icon

pyb-* icon from the custom registry, or a Material icon name.

submenu

Nested items, filtered independently.

separator

Renders a divider instead of a link.

Permission filtering is done in the template, not in the service:

<ng-container *ngIf="!item.permission" [ngTemplateOutlet]="menuItemInnerTemplate" …></ng-container>
<ng-template *ngIf="item.permission" ngxPermissionsOnly="{{ item.permission }}">
    <ng-container [ngTemplateOutlet]="menuItemInnerTemplate" …></ng-container>
</ng-template>

An item without permission is always visible; an item with one is wrapped in *ngxPermissionsOnly. See Permissions System.

The header menu tree is configured but not rendered

LayoutConfig sets header.menu.self.display: false, so the whole header tree in MenuConfig is dead configuration. It also contains several levels of self-similar nesting (Reports inside Reports inside Reports) with items that duplicate the aside entries and omit their permissions.

The only live consumer is SubheaderService.updateBreadcrumbs(), which searches the header tree first and falls back to the aside tree. Editing the header tree therefore changes breadcrumbs without changing navigation. Add new navigation to the aside tree.

Product Offerings is filtered in code, not by permission

AsideLeftComponent removes the Product Offerings entry for suppliers who are not yet activated:

private getMenuWithOrWithoutProductOfferings(list: any[], isActivated: boolean): any[] {
    if (isActivated) {
        return list;
    }
    const newList = [...list];
    const productOfferingsIndex = newList.findIndex(o => o.page === 'product-offerings');
    newList.splice(productOfferingsIndex, 1);
    return newList;
}

The item is matched by the literal string 'product-offerings', so renaming the route in URLS_ROUTER silently breaks the filter: findIndex returns -1 and splice(-1, 1) removes the last menu item instead. Activation state comes from TopBarService.details$.isActivated, not from a permission.

SubheaderService

Owns everything above the page content. It re-derives its state on every NavigationEnd.

title$

BreadcrumbTitle — title, desc, subDesc.

breadcrumbs$

Resolved breadcrumb trail.

visible$ / disabled$

Whole-subheader visibility. BaseComponent binds visible$.

smallMode$

Compact variant, via smallSubHeader() / normalSubHeader().

customPaddingBottom$

Extra bottom padding — PADDING_MOBILE (80) or PADDING_TABLET (120).

Titles resolve in this order:

  1. a manual title previously set for this exact URL by setTitle(),

  2. otherwise PageConfig for the cleaned URL,

  3. breadcrumbs from the header tree, falling back to the aside tree.

A single breadcrumb whose title matches the page title is dropped as redundant.

setTitle() is per-URL and permanent

setTitle() stores the title in a manualTitle map keyed by router.url, so it wins over PageConfig for the rest of the session. That is how sales-call-summary personalises its heading:

this.subheaderService.setTitle(`Welcome to Payability, ${this.titleCasePipe.transform(user.firstName)}!`);

The PageConfig entry for that route still contains the literal placeholder 'Welcome to Payability, Alexis!'. It is never displayed, because the component overrides it as soon as the user loads — but it is what you would see if that subscription ever failed.

Responsive behaviour

MenuAsideService owns the drawer, driven by BreakpointService:

get sidenavMode() {
    return this.breakPointService.isMobileOrTablet() ? 'over' : 'side';
}

On every breakpoint change the drawer is closed for mobile/tablet and opened for desktop. BaseComponent.onMenuOptionClick() also closes it after a navigation on small screens, so the menu does not stay over the page the user just opened.

Splash screen

The markup lives in src/index.html so it paints before Angular boots. SplashScreenService.hide() fades it out over 800 ms after a 300 ms delay and then removes the element, guarding with a stopped flag so a second call is a no-op. The logo and message come from LayoutConfig.loader.

Adding a page to the shell

  1. Add a PageConfig entry for the route key so the subheader has a title.

  2. Add a MenuConfig entry under aside.items, with page from URLS_ROUTER, a translate key, an icon, and a permission if the page is gated.

  3. Add the translation keys to src/assets/i18n/en.ts. Internationalization

See Adding a Feature for the full checklist.