feat: close stub-page gaps - profile login/logout, admin Reports/Settings, Help/Docs links
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Sprint A: storefront header profile control (login/logout only, no menu), wired to existing customer Telegram auth (AuthService). Sprint B: backoffice/reports page, reuses AdminAnalyticsFacade (Sales, Top Products, Marketplace Health cards + CSV export). Sprint C: backoffice/settings page, admin UI density preference (comfortable/compact), localStorage-persisted, applied to app-table across all admin list pages. Sprint D: admin bottom-nav Help -> mailto using existing supportEmail, Documentation -> external link via new TenantConfig.documentationUrl. AdminNavLink gains externalHref for non-routerLink nav entries. Docs: docs/GLOBAL-SPRINT-PLAN.md tracks the full sprint breakdown. docs/COMING-SOON-AUDIT.md removed, folded into docs/KNOWN-ISSUES.md. docs/BACKEND.md updated with the new documentationUrl bootstrap field. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,8 +44,20 @@
|
||||
</ul>
|
||||
|
||||
<ul class="admin-layout__nav-list admin-layout__nav-list--bottom">
|
||||
@for (entry of navBottom; track $index) {
|
||||
@if (entry.type === 'link' && entry.comingSoon) {
|
||||
@for (entry of navBottom(); track $index) {
|
||||
@if (entry.type === 'link' && entry.externalHref) {
|
||||
<li>
|
||||
<a
|
||||
class="admin-layout__nav-link"
|
||||
[href]="entry.externalHref"
|
||||
[attr.target]="entry.externalHref.startsWith('mailto:') ? null : '_blank'"
|
||||
[attr.rel]="entry.externalHref.startsWith('mailto:') ? null : 'noopener'"
|
||||
>
|
||||
<app-icon [name]="entry.icon" [size]="18" />
|
||||
<span class="admin-layout__nav-label">{{ entry.labelKey | translate }}</span>
|
||||
</a>
|
||||
</li>
|
||||
} @else if (entry.type === 'link' && entry.comingSoon) {
|
||||
<li>
|
||||
<button type="button" class="admin-layout__nav-link admin-layout__nav-link--disabled" disabled>
|
||||
<app-icon [name]="entry.icon" [size]="18" />
|
||||
@@ -151,7 +163,7 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="admin-content" class="admin-layout__content" tabindex="-1">
|
||||
<main id="admin-content" class="admin-layout__content" [class.admin-density-compact]="density() === 'compact'" tabindex="-1">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ import { LanguageService } from '../../../services/language.service';
|
||||
import { AdminAuthService } from '../../../core/admin-auth/admin-auth.service';
|
||||
import { ADMIN_NAV_BOTTOM, ADMIN_NAV_PRIMARY, AdminBreadcrumbEntry, AdminNavEntry } from './admin-nav.model';
|
||||
import { IconComponent } from '../../../shared/ui/icon/icon.component';
|
||||
import { AdminPreferencesService } from '../settings/services/admin-preferences.service';
|
||||
import { UiRuntimeFacade } from '../../../facades/runtime/ui-runtime.facade';
|
||||
import { ConfigService } from '../../../core/config/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-layout',
|
||||
@@ -25,12 +28,35 @@ export class AdminLayoutComponent {
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly languageService = inject(LanguageService);
|
||||
private readonly adminAuth = inject(AdminAuthService);
|
||||
private readonly preferences = inject(AdminPreferencesService);
|
||||
private readonly uiRuntime = inject(UiRuntimeFacade);
|
||||
private readonly configService = inject(ConfigService);
|
||||
|
||||
readonly density = this.preferences.density;
|
||||
|
||||
private readonly drawerEl = viewChild<ElementRef<HTMLElement>>('drawer');
|
||||
private readonly menuToggleEl = viewChild<ElementRef<HTMLElement>>('menuToggle');
|
||||
|
||||
readonly navPrimary: AdminNavEntry[] = ADMIN_NAV_PRIMARY;
|
||||
readonly navBottom: AdminNavEntry[] = ADMIN_NAV_BOTTOM;
|
||||
|
||||
readonly navBottom = computed<AdminNavEntry[]>(() => {
|
||||
this.configService.bootstrapRevision();
|
||||
const supportEmail = this.uiRuntime.contactEmail();
|
||||
const documentationUrl = this.configService.getBootstrapSnapshot()?.tenant?.documentationUrl;
|
||||
|
||||
return ADMIN_NAV_BOTTOM.map(entry => {
|
||||
if (entry.type !== 'link') {
|
||||
return entry;
|
||||
}
|
||||
if (entry.id === 'help' && supportEmail) {
|
||||
return { ...entry, comingSoon: false, externalHref: `mailto:${supportEmail}` };
|
||||
}
|
||||
if (entry.id === 'documentation' && documentationUrl) {
|
||||
return { ...entry, comingSoon: false, externalHref: documentationUrl };
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
});
|
||||
|
||||
readonly mobileDrawerOpen = signal(false);
|
||||
readonly notificationsOpen = signal(false);
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface AdminNavLink {
|
||||
absolutePath?: string[];
|
||||
/** No route exists yet - rendered as a disabled item with a "coming soon" badge. */
|
||||
comingSoon?: boolean;
|
||||
/** External URL (mailto: or https:). Mutually exclusive with path/absolutePath/comingSoon. */
|
||||
externalHref?: string;
|
||||
}
|
||||
|
||||
export interface AdminNavGroup {
|
||||
@@ -36,18 +38,24 @@ export const ADMIN_NAV_PRIMARY: AdminNavEntry[] = [
|
||||
{ type: 'link', id: 'customers', icon: 'user', labelKey: 'adminShell.nav.customers', path: ['customers'] },
|
||||
{ type: 'link', id: 'transactions', icon: 'creditCard', labelKey: 'adminShell.nav.transactions', path: ['transactions'] },
|
||||
{ type: 'link', id: 'moderation', icon: 'star', labelKey: 'adminShell.nav.moderation', path: ['moderation'] },
|
||||
{ type: 'link', id: 'reports', icon: 'chartBar', labelKey: 'adminShell.nav.reports', comingSoon: true },
|
||||
{ type: 'link', id: 'reports', icon: 'chartBar', labelKey: 'adminShell.nav.reports', path: ['reports'] },
|
||||
{ type: 'group', labelKey: 'adminShell.nav.partnersGroup' },
|
||||
{ type: 'link', id: 'seller-management', icon: 'store', labelKey: 'adminShell.nav.sellerManagement', path: ['partners', 'seller-management'] },
|
||||
{ type: 'link', id: 'content', icon: 'edit', labelKey: 'adminShell.nav.content', absolutePath: ['edit', 'static-pages'] },
|
||||
{ type: 'link', id: 'media', icon: 'images', labelKey: 'adminShell.nav.mediaLibrary', path: ['media'] },
|
||||
{ type: 'link', id: 'marketplace-builder', icon: 'network', labelKey: 'adminShell.nav.marketplaceBuilder', absolutePath: ['edit'] },
|
||||
{ type: 'link', id: 'users', icon: 'users', labelKey: 'adminShell.nav.users', path: ['users'] },
|
||||
{ type: 'link', id: 'settings', icon: 'settings', labelKey: 'adminShell.nav.settings', comingSoon: true },
|
||||
{ type: 'link', id: 'settings', icon: 'settings', labelKey: 'adminShell.nav.settings', path: ['settings'] },
|
||||
{ type: 'link', id: 'monitoring', icon: 'monitor', labelKey: 'adminShell.nav.monitoring', path: ['monitoring'] },
|
||||
{ type: 'link', id: 'analytics', icon: 'chartLine', labelKey: 'adminShell.nav.analytics', path: ['analytics'] },
|
||||
];
|
||||
|
||||
/**
|
||||
* `documentation`/`help` externalHref is resolved at runtime from bootstrap
|
||||
* config (tenant.documentationUrl / branding.supportEmail) — see
|
||||
* `AdminLayoutComponent.navBottom`. Static comingSoon here is the fallback
|
||||
* when that data is absent.
|
||||
*/
|
||||
export const ADMIN_NAV_BOTTOM: AdminNavEntry[] = [
|
||||
{ type: 'link', id: 'documentation', icon: 'book', labelKey: 'adminShell.nav.documentation', comingSoon: true },
|
||||
{ type: 'link', id: 'help', icon: 'help', labelKey: 'adminShell.nav.help', comingSoon: true },
|
||||
|
||||
Reference in New Issue
Block a user