# Marketplace Admin Dashboard - Sprint 19 ## Scope Sprint 19 adds the production Admin Dashboard and makes it the default landing page for the admin area. It also wires the previously-unrouted `admin/products` feature and adds route placeholders for backoffice sections that don't have a feature built yet. ## Routing All admin routes live under `/:lang/backoffice/**` (`app.routes.ts`), guarded by the existing `adminAuthGuard` (`core/admin-auth/admin-auth.guard.ts`): ```text /:lang/backoffice -> redirects to dashboard /:lang/backoffice/dashboard -> AdminDashboardPageComponent /:lang/backoffice/products -> AdminProductsListPageComponent /:lang/backoffice/products/create -> AdminProductEditorPageComponent /:lang/backoffice/products/:id/edit -> AdminProductEditorPageComponent /:lang/backoffice/products/:id/duplicate -> AdminProductEditorPageComponent /:lang/backoffice/categories -> AdminCategoriesListPageComponent /:lang/backoffice/categories/create -> AdminCategoryEditorPageComponent /:lang/backoffice/categories/:id/edit -> AdminCategoryEditorPageComponent /:lang/backoffice/static-pages -> BackofficeComingSoonPageComponent /:lang/backoffice/transactions -> BackofficeComingSoonPageComponent /:lang/backoffice/orders -> BackofficeComingSoonPageComponent /:lang/backoffice/media -> BackofficeComingSoonPageComponent ``` `admin/products` (`features/admin/products/`) was already fully implemented in an earlier sprint but was never wired into `app.routes.ts` and its internal navigation hardcoded the `ru` locale segment. Both are fixed in this sprint: routes are wired, and `admin-products-list-page.component.ts` / `admin-product-editor-page.component.ts` now build the locale segment from `LanguageService.currentLanguage()`. **Dashboard as default admin page:** on successful admin Telegram QR login, `TelegramLoginComponent` (`mode="admin"`) navigates to `/:lang/backoffice/dashboard` (`components/telegram-login/telegram-login.component.ts`). The `backoffice` route's empty path also redirects to `dashboard`, so any bare `/:lang/backoffice` link lands there too. ## Architecture ```text src/app/features/admin/dashboard/ models/ admin-dashboard.model.ts services/ admin-dashboard-metrics.gateway.interface.ts admin-dashboard-metrics.local.gateway.ts admin-dashboard-metrics-gateway.token.ts admin-dashboard-history.service.ts facade/ admin-dashboard.facade.ts components/ admin-dashboard-card.component.* admin-dashboard-quick-actions.component.* admin-dashboard-activity.component.* admin-dashboard-health.component.* pages/ admin-dashboard-page.component.* src/app/features/backoffice/shared/ backoffice-coming-soon-page.component.* ``` Follows the existing container/facade/service split (ADR-006, ADR-007): `AdminDashboardPageComponent` is the container, `AdminDashboardFacade` owns orchestration, presentational card/quick-actions/activity/health components take only `@Input()`s and have no HttpClient/localStorage/route access. ### Data sources (future-ready) Cards never read `ConfigService`, `localStorage`, or an HTTP client directly - everything routes through `AdminDashboardFacade`, which composes: - **`ProjectEditorFacade`** (already existed) - `bootstrap`, `status`, `lastSavedAt`, `lastPublishedAt` (new, see below), `validationIssues`, `homepageWidgets`. Backs Marketplace Status, Project Name, Current Theme, Languages, Last Publish, Last Draft Save, Bootstrap Version, Active Layout, Enabled Widgets, and the System Health checks. - **`ADMIN_DASHBOARD_METRICS_GATEWAY`** (new `InjectionToken`, same swap pattern as `BACKOFFICE_DATA_PROVIDER`) - defaults to `AdminDashboardMetricsLocalGateway`, which composes `BackofficeDataService.loadCategories()/loadProducts()` (already used by `AdminProductsLocalGateway`) into counts. Backs Categories Count and Products Count. Swapping to a real dashboard-metrics endpoint later means implementing `AdminDashboardMetricsGateway` and rebinding the token - the facade and cards don't change. - **`AdminDashboardHistoryService`** (new) - localStorage-backed activity log, scoped per tenant, same pattern as `ProjectEditorDraftStorageService`. The facade appends an entry whenever `lastSavedAt`/`lastPublishedAt` change (detected via an `effect()`, primed on first read so the initial bootstrap load doesn't get logged as an activity event). Backs Recent Activity. ### Orders / Revenue No backend or local data model exists for orders or revenue anywhere in the codebase (`features/backoffice/orders` is an empty placeholder folder). These two cards render an honest **`pending-backend`** card state ("Awaiting backend integration") rather than fabricated numbers - not a "no data" empty state, since the gap is structural, not a temporarily-empty dataset. ### Card states `AdminDashboardCardComponent` (`components/admin-dashboard-card.component.ts`) renders one of: `loading` (skeleton), `empty`, `error`, `pending-backend`, or the ready value + optional subtitle. The container computes each card's status per data source (bootstrap not yet loaded -> `loading`; metrics gateway error -> `error`; no supported locales -> `empty`; Orders/Revenue -> always `pending-backend`). ### System Health `ProjectValidator` (`features/project-editor/services/project-validator.service.ts`) already covered 5 of the 6 required checks. This sprint added two more: - `translationIssues()` - flags a supported non-default locale missing a header nav label translation or a static-page `translations` entry. - `layoutIssues()` - flags `bootstrap.layout.type` or any section's `layout.strategy` that isn't one of the known enum values (`PlatformLayoutType` / `SectionLayoutStrategy`). Runtime validation matters here because bootstrap JSON isn't type-checked at load time. Dashboard mapping (`AdminDashboardFacade.healthChecks`): | Dashboard label | Validator code | |---|---| | Bootstrap valid | structural: `bootstrap !== null && schemaVersion` set | | Configuration valid | no validation issues at all | | Missing translations | `missing-translations` (new) | | Invalid colors | `invalid-colors` (existing) | | Invalid widget references | `missing-widget` (existing - a homepage widget with no `type`) | | Invalid layouts | `invalid-layouts` (new) | ### Quick Actions Static list in `AdminDashboardFacade` (`route` arrays relative to the lang root); the page component prefixes the current locale (`LanguageService.currentLanguage()`) before binding `routerLink`. Categories, Static Pages, Transactions, Orders, and Media Library currently land on `BackofficeComingSoonPageComponent` since those features aren't built yet - this is a routing placeholder, not a dashboard card placeholder. ### `lastPublishedAt` (ProjectEditorFacade change) Before this sprint, `publish()` only updated `lastSavedAt`, so "last draft save" and "last publish" were indistinguishable after a publish. Added `lastPublishedAt: number | null` to `ProjectEditorState` / `ProjectEditorFacade`, set only inside `publish()`. `lastSavedAt` behavior is unchanged (still updated by both `save()` and `publish()`). ## Sprint 20 - Category Management `features/admin/categories/` (model/gateway/facade/pages/components), same container/facade/service split as `admin/products` and `admin/dashboard`: ```text src/app/features/admin/categories/ models/ admin-category.model.ts services/ admin-categories-gateway.interface.ts admin-categories-local.gateway.ts admin-categories-form.factory.ts facade/ admin-categories.facade.ts guards/ admin-category-dirty.guard.ts components/ admin-categories-list.component.* admin-category-form.component.* pages/ admin-categories-list-page.component.ts admin-category-editor-page.component.ts ``` - **Hierarchy**: `AdminCategory.parentId` (nullable). List page renders a flattened, indented tree (`AdminCategoriesFacade.rootCategories()` / `childrenOf(id)`); the editor's parent `` still uses `AdminProductsGateway.loadCategories()` (its own `AdminProductCategoryOption` seed), not `AdminCategoriesGateway` - unifying them is Sprint 21 scope (`docs/SPRINT-PLAN.md`). ## Known gaps / backend needs - **Dashboard metrics endpoint.** Categories/Products counts are computed client-side from `BackofficeDataService` (itself mock/API-switchable via `BACKOFFICE_DATA_PROVIDER`). A dedicated `/builder/dashboard/summary`-style endpoint would let `AdminDashboardMetricsGateway` return richer data (real-time counts, trend deltas) without touching the facade or cards. - **Orders/Revenue have no backend at all** (see above) - needs an order domain and revenue aggregation before these cards can show real data. - **Recent Activity is local-only**, scoped to the browser/tenant via localStorage (`adminDashboard.activityHistory.v1`), same limitation as the existing draft-save local storage. It will not show another editor's activity until a real audit-log endpoint exists. - **Admin authorization is still not enforced server-side** (see `Project-Editor.md` - "Admin Authentication" section); this sprint does not change that. Nothing new here beyond routing/dashboard.