diff --git a/docs/BACKEND.md b/docs/BACKEND.md
index 4eefb80..f347041 100644
--- a/docs/BACKEND.md
+++ b/docs/BACKEND.md
@@ -168,8 +168,12 @@ Each nested field, with its source model file under
- **`tenant`** (`TenantConfig`, `tenant.model.ts`) —
`{ id (UUID), slug, code, host, name, websiteBaseUrl, builderBaseUrl,
backofficeBaseUrl, defaultLocale, supportedLocales[], defaultCurrency,
- supportedCurrencies[], timezone }`. Identifies the tenant and its per-surface
- base URLs, locale/currency sets, and timezone.
+ supportedCurrencies[], timezone, documentationUrl? }`. Identifies the tenant
+ and its per-surface base URLs, locale/currency sets, and timezone.
+ `documentationUrl` (optional string) is the external docs link rendered by
+ the backoffice "Documentation" nav item — when absent, that nav item shows
+ as a disabled `comingSoon` entry instead of a link (frontend-only fallback,
+ no backend action required beyond optionally sending the field).
- **`branding`** (`BrandingConfig`, `branding.model.ts`) —
`{ brandName, legalName, slogan?, logoUrl, logoCompactUrl?, faviconUrl,
appIconUrl?, socialImageUrl?, galleryUrls?, supportEmail?, supportPhone? }`.
@@ -304,7 +308,8 @@ trimmed for length; full versions in that file):
"supportedLocales": ["ru", "en", "hy"],
"defaultCurrency": "RUB",
"supportedCurrencies": ["RUB", "USD", "EUR", "AMD"],
- "timezone": "Europe/Moscow"
+ "timezone": "Europe/Moscow",
+ "documentationUrl": "https://docs.marketplace.local"
},
"branding": {
"brandName": "Marketplace",
diff --git a/docs/EDITOR.md b/docs/EDITOR.md
index 0fdd25b..d9169ed 100644
--- a/docs/EDITOR.md
+++ b/docs/EDITOR.md
@@ -158,6 +158,6 @@ A section-by-section correctness audit (not a feature pass) — for each section
### Known gaps found but not fixed (real, out of scope for this pass)
-- **Theme Mode has no runtime effect.** `theme-section`'s light/dark/system selector correctly saves and sets a `data-theme-mode` attribute (`theme-engine.service.ts`), but zero CSS anywhere in the app reads that attribute — picking Dark or System currently changes nothing visually. (Theme palette colors *are* live — real CSS custom properties consumed throughout the stylesheets — only the mode switch is dead.) Fixing this is a real dark-mode implementation project (dark palette + CSS strategy + `matchMedia` for "system"), not a wiring fix.
-- **`layout.type` (Site Layout) and the homepage section's `type` field both feed a rendering pipeline that was never wired up.** `src/app/dynamic-renderer/` has services/models for page/section/widget rendering but zero components or templates (every directory has only a `.gitkeep`) — the storefront homepage renders through a separate, older path that ignores both fields. `homepage-section.component.ts`'s `updateSection(id, 'type', ...)` has no UI calling it because of this; not built, since building UI for a field nothing reads would be inventing dead controls.
-- **`HeaderConfig.showProfile`** is a real toggle in `header-section` with no corresponding profile/account menu anywhere in `header.component.html` — the toggle currently does nothing. Building the actual menu is a feature (needs an auth-system check first), not an editor-wiring fix.
+- **Theme Mode has no runtime effect.** `theme-section`'s light/dark/system selector correctly saves and sets a `data-theme-mode` attribute (`theme-engine.service.ts`), but zero CSS anywhere in the app reads that attribute — picking Dark or System currently changes nothing visually. (Theme palette colors *are* live — real CSS custom properties consumed throughout the stylesheets — only the mode switch is dead.) Fixing this is a real dark-mode implementation project (dark palette + CSS strategy + `matchMedia` for "system"), not a wiring fix. Tracked: `docs/PRODUCT_BACKLOG.md`.
+- ~~`HeaderConfig.showProfile` has no corresponding profile/account menu~~ — **fixed**: `header.component.html`/`.ts` now render a login/logout-only control (no dropdown, no account links) gated by this toggle, reusing the customer Telegram `AuthService`. See `docs/KNOWN-ISSUES.md` "Fixed (this cycle)" and `docs/GLOBAL-SPRINT-PLAN.md` Sprint A.
+- ~~`layout.type`/homepage `type` field feed an unwired `dynamic-renderer/`~~ — **stale, corrected**: `dynamic-renderer/` (`PageRendererService`/`SectionRendererService`/`WidgetHostService`) is the live homepage rendering pipeline, wired through `dynamic-page-layout.component.ts`. Verified fixed/non-issue in `docs/KNOWN-ISSUES.md` "Fixed (this cycle)".
diff --git a/docs/GLOBAL-SPRINT-PLAN.md b/docs/GLOBAL-SPRINT-PLAN.md
new file mode 100644
index 0000000..8c14c48
--- /dev/null
+++ b/docs/GLOBAL-SPRINT-PLAN.md
@@ -0,0 +1,51 @@
+# Global Sprint Plan — "Coming Soon" Stub Closure
+
+Supersedes `docs/COMING-SOON-AUDIT.md` §5 sprint breakdown. One consolidated tracker for the four stub-closure sprints. Approved decisions (from AskUserQuestion): Reports/Settings ship as minimal real pages (not fake data, not empty shells); Documentation/Help nav uses an external-link approach; `docs/COMING-SOON-AUDIT.md` is deleted once all sprints land, folded into `docs/KNOWN-ISSUES.md`. Profile control constraint: **login/logout only — no dropdown, no account links.**
+
+## Sprint A — Profile menu (storefront header)
+
+- [x] i18n: `header.login` / `header.logout` keys in en/ru/hy (`translations.ts` type already updated)
+- [x] `header.component.ts`: inject `AuthService`, expose `isAuthenticated`, add `login()`/`logout()`
+- [x] `header.component.ts`: import `TelegramLoginComponent`
+- [x] `header.component.html`: profile control gated by `headerConfig().showProfile`, login/logout only, `` rendered once
+- [x] SCSS matches existing header button conventions (reused `.platform-ux-btn`, no new SCSS needed)
+
+**What shipped:** Header profile control wired to the customer `AuthService` (Telegram QR login). Gated by `headerConfig().showProfile` (already a real toggle in Project Editor, previously dead). Logged-out shows a login button (`user` icon), logged-in shows a logout button (`logOut` icon) — no dropdown, no account links, per the explicit constraint.
+
+## Sprint B — Admin Reports page
+
+- [x] `admin-reports-page.component.ts/.html/.scss` (mirrors `admin-analytics-page` structure), reuses `AdminAnalyticsFacade`
+- [x] Report cards: Sales, Top Products, Marketplace Health
+- [x] CSV export wired to existing facade export methods / existing download helper (same Blob pattern as `admin-analytics-page.component.ts`)
+- [x] Route `backoffice/reports` in `app.routes.ts`, i18n keys `adminShell.pages.reports.*` + new `adminReports.*` block
+- [x] Remove `comingSoon: true` from `reports` nav entry
+
+**What shipped:** Minimal real Reports page with 3 cards (Sales, Top Products, Marketplace Health), each showing a live summary from `AdminAnalyticsFacade` and a CSV export button. Orders card was scoped out — see final report for why (reuse would require mutating a shared singleton facade's pagination state).
+
+## Sprint C — Admin Settings page
+
+- [x] `AdminPreferencesService` (density signal, localStorage-backed, key `adminPreferences.density.v1`)
+- [x] `admin-layout.component` applies `admin-density-compact` class to `#admin-content` shell wrapper
+- [x] `admin-settings-page.component.ts/.html/.scss` — density toggle (`app-toggle`), auto-persists on change, no separate Save button
+- [x] Route `backoffice/settings`, i18n keys `adminShell.pages.settings.*` + `adminSettings.*` block
+- [x] Remove `comingSoon: true` from nav entry AND dashboard shortcut; shortcut route → `['backoffice','settings']`
+- [x] Compact-density CSS rule added to the shared `app-table` component stylesheet (`.admin-density-compact .app-table th/td`) — applies to every admin list page built on `app-table` (orders, products, categories, etc.), not just one
+
+**What shipped:** Genuinely real, backend-independent UI density preference. No maintenance-mode toggle built (explicitly deferred per `docs/NEXT_PHASE.md` Phase 4).
+
+## Sprint D — Documentation / Help nav
+
+- [x] Help: `mailto:` using existing `supportEmail` read path (`UiRuntimeFacade.contactEmail()`, same one `header.component.ts` already uses for `bootstrap.branding.supportEmail`)
+- [x] `AdminNavLink` gains optional `externalHref?: string`; nav renderer renders `` branch (bottom nav)
+- [x] Documentation: added `tenant.documentationUrl?: string` to `TenantConfig`, populated mock with `https://docs.marketplace.local`
+- [x] `help`/`documentation` resolved dynamically in `admin-layout.component.ts` (`navBottom` computed) — real `` when bootstrap data present, static `comingSoon: true` entries kept as defensive fallback for the (currently unreachable, since mock always has both fields) case where the backend omits them
+
+**What shipped:** Both Help and Documentation wired to real external links, not just Help. `comingSoon: true` remains in `admin-nav.model.ts` source as a fallback flag only — it is overridden to `false` at render time whenever bootstrap actually has the data, which it does today.
+
+## Housekeeping
+
+- [ ] Delete `docs/COMING-SOON-AUDIT.md`
+- [ ] Fold summary into `docs/KNOWN-ISSUES.md` "Fixed (this cycle)"; remove the `HeaderConfig.showProfile` dead-toggle entry from "Open"
+- [ ] Update `docs/BACKEND.md` (and `docs/backend/BACKEND-INTEGRATION.md` if applicable) for `tenant.documentationUrl` only, if added
+- [ ] `npm run barry -- validate`
+- [ ] Typecheck touched files
diff --git a/docs/KNOWN-ISSUES.md b/docs/KNOWN-ISSUES.md
index a3883e7..286b9c6 100644
--- a/docs/KNOWN-ISSUES.md
+++ b/docs/KNOWN-ISSUES.md
@@ -39,3 +39,7 @@ Condensed — full detail in commit history and `docs/RELEASE_REPORT.md`.
- `primeng`/`primeicons` unused dependency — removed.
- Builder static-page body editor hidden inside a mislabeled collapsed section — un-hidden, relabeled.
- Several project-editor/admin-categories correctness bugs (footer icon id collisions, features toggle only driving one flag, languages silent duplicate no-op, static-pages slug collision, branding `socialImageUrl` never read, media-picker facade filter leakage between dialogs, categories draft-recovery/drag-reorder bugs, hardcoded locale-tab order) — see git history for the full per-bug list.
+- `HeaderConfig.showProfile` dead toggle — wired up (login/logout only, no dropdown), reuses the existing customer Telegram `AuthService`.
+- Admin `reports` nav stub — real page (`backoffice/reports`), reuses `AdminAnalyticsFacade` for Sales/Top Products/Marketplace Health cards with CSV export.
+- Admin `settings` nav stub — real page (`backoffice/settings`), UI density preference (comfortable/compact), persisted to `localStorage`, applied to admin list tables.
+- Admin `documentation`/`help` nav stubs — both wired to real external links (`mailto:` support email, `tenant.documentationUrl`).
diff --git a/docs/NEXT_PHASE.md b/docs/NEXT_PHASE.md
index 28b4036..89e5e26 100644
--- a/docs/NEXT_PHASE.md
+++ b/docs/NEXT_PHASE.md
@@ -20,4 +20,4 @@ Wire real error tracking/APM and a real event source for the admin Monitoring pa
## Phase 5 — Version 2 ideas
-Everything in `docs/PRODUCT_BACKLOG.md` (dark mode, brand-color contrast decision, advanced analytics, additional payment providers, Contacts page content) and `docs/FUTURE_FEATURES.md` (Angular 22 upgrade, cart-modal composition cleanup) — none of it scheduled, all of it deliberately deferred past initial launch.
+Everything in `docs/PRODUCT_BACKLOG.md` (dark mode, brand-color contrast decision, advanced analytics, additional payment providers, Contacts page content) and `docs/FUTURE_FEATURES.md` (Angular 22 upgrade, cart-modal composition cleanup) — none of it scheduled, all of it deliberately deferred past initial launch. The former stub-page/dead-toggle inventory (profile menu, admin Reports, admin Settings, Documentation/Help) is closed — see `docs/GLOBAL-SPRINT-PLAN.md` and `docs/KNOWN-ISSUES.md` "Fixed (this cycle)".
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index f4bf1a5..1d5ba92 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -254,6 +254,24 @@ const coreRoutes: Routes = [
breadcrumb: [{ labelKey: 'adminShell.nav.analytics' }]
}
},
+ {
+ path: 'reports',
+ loadComponent: () => import('./features/admin/reports/pages/admin-reports-page.component').then(m => m.AdminReportsPageComponent),
+ data: {
+ titleKey: 'adminShell.pages.reports.title',
+ descriptionKey: 'adminShell.pages.reports.description',
+ breadcrumb: [{ labelKey: 'adminShell.nav.reports' }]
+ }
+ },
+ {
+ path: 'settings',
+ loadComponent: () => import('./features/admin/settings/pages/admin-settings-page.component').then(m => m.AdminSettingsPageComponent),
+ data: {
+ titleKey: 'adminShell.pages.settings.title',
+ descriptionKey: 'adminShell.pages.settings.description',
+ breadcrumb: [{ labelKey: 'adminShell.nav.settings' }]
+ }
+ },
{
path: 'partners/seller-management',
loadComponent: () => import('./features/admin/seller-management/pages/admin-seller-management-page.component').then(m => m.AdminSellerManagementPageComponent),
diff --git a/src/app/components/header/header.component.html b/src/app/components/header/header.component.html
index 556a79f..6884ed9 100644
--- a/src/app/components/header/header.component.html
+++ b/src/app/components/header/header.component.html
@@ -80,6 +80,19 @@
}
+
+ @if (headerConfig().showProfile) {
+ @if (isAuthenticated()) {
+
+ } @else {
+
+ }
+ }
+
@if (headerConfig().showRegion) {
@@ -147,4 +160,6 @@
+
+
diff --git a/src/app/components/header/header.component.ts b/src/app/components/header/header.component.ts
index 142f5ed..0e4ed93 100644
--- a/src/app/components/header/header.component.ts
+++ b/src/app/components/header/header.component.ts
@@ -14,10 +14,12 @@ import { FeatureConfigService } from '../../core/config/feature-config.service';
import { DEFAULT_HEADER_CONFIG, DEFAULT_USER_EXPERIENCE_CONFIG } from '../../shared/models/config';
import { StaticPageResolverService } from '../../core/config/static-page-resolver.service';
import { IconComponent } from '../../shared/ui/icon/icon.component';
+import { AuthService } from '../../services/auth.service';
+import { TelegramLoginComponent } from '../telegram-login/telegram-login.component';
@Component({
selector: 'app-header',
- imports: [RouterLink, RouterLinkActive, LogoComponent, LanguageSelectorComponent, RegionSelectorComponent, LangRoutePipe, TranslatePipe, IconComponent],
+ imports: [RouterLink, RouterLinkActive, LogoComponent, LanguageSelectorComponent, RegionSelectorComponent, LangRoutePipe, TranslatePipe, IconComponent, TelegramLoginComponent],
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@@ -35,7 +37,9 @@ export class HeaderComponent {
private configService = inject(ConfigService);
private featureConfig = inject(FeatureConfigService);
private staticPageResolver = inject(StaticPageResolverService);
+ private authService = inject(AuthService);
+ readonly isAuthenticated = this.authService.isAuthenticated;
readonly wishlistCount = this.uxFacade.wishlistCount;
readonly compareCount = this.uxFacade.compareCount;
readonly userExperienceConfig = computed(() => this.resolveUserExperienceConfig());
@@ -118,6 +122,14 @@ export class HeaderComponent {
this.router.navigate([`/${lang}/compare`]);
}
+ login(): void {
+ this.authService.requestLogin();
+ }
+
+ logout(): void {
+ this.authService.logout();
+ }
+
navigateToStatic(route: string): void {
this.closeMenu();
const lang = this.langService.currentLanguage();
diff --git a/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts b/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts
index 526eeca..37c60c9 100644
--- a/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts
+++ b/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts
@@ -31,7 +31,7 @@ const SHORTCUTS: AdminDashboardShortcut[] = [
{ id: 'static-pages', icon: 'edit', labelKey: 'dashboard.actionStaticPages', route: ['backoffice', 'static-pages'] },
{ id: 'orders', icon: 'cart', labelKey: 'dashboard.actionOrders', route: ['backoffice', 'orders'] },
{ id: 'users', icon: 'users', labelKey: 'dashboard.actionUsers', route: ['backoffice', 'users'] },
- { id: 'settings', icon: 'settings', labelKey: 'dashboard.shortcutSettings', route: [], comingSoon: true },
+ { id: 'settings', icon: 'settings', labelKey: 'dashboard.shortcutSettings', route: ['backoffice', 'settings'] },
{ id: 'media-library', icon: 'images', labelKey: 'dashboard.actionMediaLibrary', route: ['backoffice', 'media'] },
{ id: 'content', icon: 'alignLeft', labelKey: 'dashboard.shortcutContent', route: ['edit', 'static-pages'] },
];
diff --git a/src/app/features/admin/reports/pages/admin-reports-page.component.html b/src/app/features/admin/reports/pages/admin-reports-page.component.html
new file mode 100644
index 0000000..84e69c6
--- /dev/null
+++ b/src/app/features/admin/reports/pages/admin-reports-page.component.html
@@ -0,0 +1,32 @@
+
+ @if (facade.loading()) {
+
+ @for (i of [1,2,3]; track i) {
+
+ }
+
{{ 'common.loading' | translate }}
+
+ } @else {
+
+
+
{{ 'adminReports.sales' | translate }}
+ @if (facade.summary(); as summary) {
+
{{ summary.revenueTotal }} {{ summary.currency }} · {{ summary.ordersCount }} {{ 'adminAnalytics.orders' | translate }}
+ }
+
{{ 'adminOrders.export' | translate }}
+
+
+
+
{{ 'adminReports.topProducts' | translate }}
+
{{ facade.topProducts().length }} {{ 'adminAnalytics.topProducts' | translate }}
+
{{ 'adminOrders.export' | translate }}
+
+
+
+
{{ 'adminReports.marketplaceHealth' | translate }}
+
{{ facade.healthCompletionPercent() }}% {{ 'adminMarketplaceHealth.complete' | translate }}
+
{{ 'adminOrders.export' | translate }}
+
+
+ }
+
diff --git a/src/app/features/admin/reports/pages/admin-reports-page.component.scss b/src/app/features/admin/reports/pages/admin-reports-page.component.scss
new file mode 100644
index 0000000..39a8432
--- /dev/null
+++ b/src/app/features/admin/reports/pages/admin-reports-page.component.scss
@@ -0,0 +1,19 @@
+.admin-reports-page { display: grid; gap: 16px; padding: 16px; max-width: 1100px; margin: 0 auto; }
+
+.report-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
+
+.report-card {
+ display: grid;
+ gap: 10px;
+ align-content: start;
+ padding: 16px;
+ border: 1px solid var(--border-color, #d3dad9);
+ border-radius: var(--radius-md);
+ background: var(--bg-primary, #fff);
+}
+.report-card h2 { margin: 0; font-size: var(--font-size-xl, 1.125rem); }
+.report-summary { margin: 0; color: var(--text-secondary, #6b7280); font-size: var(--font-size-sm, 0.8125rem); }
+
+@media (max-width: 900px) {
+ .report-grid { grid-template-columns: 1fr; }
+}
diff --git a/src/app/features/admin/reports/pages/admin-reports-page.component.ts b/src/app/features/admin/reports/pages/admin-reports-page.component.ts
new file mode 100644
index 0000000..cc5e6ef
--- /dev/null
+++ b/src/app/features/admin/reports/pages/admin-reports-page.component.ts
@@ -0,0 +1,43 @@
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { AdminAnalyticsFacade } from '../../analytics/facade/admin-analytics.facade';
+import { TranslatePipe } from '../../../../i18n/translate.pipe';
+import { ButtonComponent } from '../../../../shared/ui/button/button.component';
+import { SkeletonComponent } from '../../../../shared/ui/skeleton/skeleton.component';
+
+@Component({
+ selector: 'app-admin-reports-page',
+ standalone: true,
+ imports: [TranslatePipe, ButtonComponent, SkeletonComponent],
+ templateUrl: './admin-reports-page.component.html',
+ styleUrls: ['./admin-reports-page.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class AdminReportsPageComponent {
+ readonly facade = inject(AdminAnalyticsFacade);
+
+ constructor() {
+ this.facade.load();
+ }
+
+ exportSalesCsv(): void {
+ this.download(this.facade.exportCsv(), 'sales-report.csv');
+ }
+
+ exportTopProductsCsv(): void {
+ this.download(this.facade.exportTopProductsCsv(), 'top-products-report.csv');
+ }
+
+ exportHealthCsv(): void {
+ this.download(this.facade.exportHealthCsv(), 'marketplace-health-report.csv');
+ }
+
+ private download(csv: string, filename: string): void {
+ const blob = new Blob([csv], { type: 'text/csv' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = filename;
+ link.click();
+ URL.revokeObjectURL(url);
+ }
+}
diff --git a/src/app/features/admin/settings/pages/admin-settings-page.component.html b/src/app/features/admin/settings/pages/admin-settings-page.component.html
new file mode 100644
index 0000000..619a953
--- /dev/null
+++ b/src/app/features/admin/settings/pages/admin-settings-page.component.html
@@ -0,0 +1,14 @@
+
+
+
{{ 'adminSettings.density' | translate }}
+
{{ 'adminSettings.densityExplain' | translate }}
+
+
+
diff --git a/src/app/features/admin/settings/pages/admin-settings-page.component.scss b/src/app/features/admin/settings/pages/admin-settings-page.component.scss
new file mode 100644
index 0000000..67d3c18
--- /dev/null
+++ b/src/app/features/admin/settings/pages/admin-settings-page.component.scss
@@ -0,0 +1,14 @@
+.admin-settings-page { display: grid; gap: 16px; padding: 16px; max-width: 720px; margin: 0 auto; }
+
+.settings-card {
+ display: grid;
+ gap: 10px;
+ padding: 16px;
+ border: 1px solid var(--border-color, #d3dad9);
+ border-radius: var(--radius-md);
+ background: var(--bg-primary, #fff);
+}
+.settings-card h2 { margin: 0; font-size: var(--font-size-xl, 1.125rem); }
+.settings-explain { margin: 0; color: var(--text-secondary, #6b7280); font-size: var(--font-size-sm, 0.8125rem); }
+
+.toggle-row { display: flex; align-items: center; gap: 8px; font-weight: var(--font-weight-normal, 400); }
diff --git a/src/app/features/admin/settings/pages/admin-settings-page.component.ts b/src/app/features/admin/settings/pages/admin-settings-page.component.ts
new file mode 100644
index 0000000..de90235
--- /dev/null
+++ b/src/app/features/admin/settings/pages/admin-settings-page.component.ts
@@ -0,0 +1,21 @@
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { AdminPreferencesService } from '../services/admin-preferences.service';
+import { TranslatePipe } from '../../../../i18n/translate.pipe';
+import { ToggleComponent } from '../../../../shared/ui/toggle/toggle.component';
+
+@Component({
+ selector: 'app-admin-settings-page',
+ standalone: true,
+ imports: [FormsModule, TranslatePipe, ToggleComponent],
+ templateUrl: './admin-settings-page.component.html',
+ styleUrls: ['./admin-settings-page.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class AdminSettingsPageComponent {
+ readonly preferences = inject(AdminPreferencesService);
+
+ onCompactToggle(compact: boolean): void {
+ this.preferences.setDensity(compact ? 'compact' : 'comfortable');
+ }
+}
diff --git a/src/app/features/admin/settings/services/admin-preferences.service.ts b/src/app/features/admin/settings/services/admin-preferences.service.ts
new file mode 100644
index 0000000..1c2c272
--- /dev/null
+++ b/src/app/features/admin/settings/services/admin-preferences.service.ts
@@ -0,0 +1,24 @@
+import { Injectable, Signal, inject, signal } from '@angular/core';
+import { LocalStorageService } from '../../../../core/storage/local-storage.service';
+
+export type AdminUiDensity = 'comfortable' | 'compact';
+
+const DENSITY_KEY = 'adminPreferences.density.v1';
+
+@Injectable({ providedIn: 'root' })
+export class AdminPreferencesService {
+ private readonly localStorage = inject(LocalStorageService);
+
+ private readonly densitySignal = signal(this.readStoredDensity());
+
+ readonly density: Signal = this.densitySignal.asReadonly();
+
+ setDensity(value: AdminUiDensity): void {
+ this.densitySignal.set(value);
+ this.localStorage.setItem(DENSITY_KEY, value);
+ }
+
+ private readStoredDensity(): AdminUiDensity {
+ return this.localStorage.getItem(DENSITY_KEY) === 'compact' ? 'compact' : 'comfortable';
+ }
+}
diff --git a/src/app/features/admin/shell/admin-layout.component.html b/src/app/features/admin/shell/admin-layout.component.html
index f94a950..9396f75 100644
--- a/src/app/features/admin/shell/admin-layout.component.html
+++ b/src/app/features/admin/shell/admin-layout.component.html
@@ -44,8 +44,20 @@
- @for (entry of navBottom; track $index) {
- @if (entry.type === 'link' && entry.comingSoon) {
+ @for (entry of navBottom(); track $index) {
+ @if (entry.type === 'link' && entry.externalHref) {
+ -
+
+
+ {{ entry.labelKey | translate }}
+
+
+ } @else if (entry.type === 'link' && entry.comingSoon) {
-