Sprint 21. - archived (soft archive/restore, distinct from visible) with an include-archived list filter - barcode field alongside sku - variants: lightweight name|price|quantity list, same textarea-parse convention as specifications/attributes - relatedProductIds: checkbox picker in the editor - gallery images now added/removed via the shared MediaPickerComponent instead of a raw URL textarea - read-only discounted-price preview in the editor - infinite-scroll toggle on the list (loadMore() appends a page instead of replacing it; pagination UI swaps for a Load more button) - category dropdown now sourced from AdminCategoriesGateway (Sprint 20) instead of AdminProductsLocalGateway's own BackofficeDataService seed docs/ADMIN.md + docs/BACKEND.md updated with the new field list and the known trade-off that related-products search is scoped to the currently loaded page, not the full catalog. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
13 KiB
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):
/: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
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(newInjectionToken, same swap pattern asBACKOFFICE_DATA_PROVIDER) - defaults toAdminDashboardMetricsLocalGateway, which composesBackofficeDataService.loadCategories()/loadProducts()(already used byAdminProductsLocalGateway) into counts. Backs Categories Count and Products Count. Swapping to a real dashboard-metrics endpoint later means implementingAdminDashboardMetricsGatewayand rebinding the token - the facade and cards don't change.AdminDashboardHistoryService(new) - localStorage-backed activity log, scoped per tenant, same pattern asProjectEditorDraftStorageService. The facade appends an entry wheneverlastSavedAt/lastPublishedAtchange (detected via aneffect(), 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-pagetranslationsentry.layoutIssues()- flagsbootstrap.layout.typeor any section'slayout.strategythat 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:
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<select>excludes the category itself and its descendants to prevent cycles. - Reordering: native HTML5 drag-and-drop in
admin-categories-list.component.ts(draggable,dragstart/drop), persists viaAdminCategoriesFacade.reorder()which just rewritesorder. - Delete/restore: soft delete (
deletedAttimestamp). Blocked client-side (facade.canDelete()) if the category has children oritemsCount > 0; list has an "include deleted" filter with a Restore action for soft-deleted rows. - Draft/publish:
status: 'draft' | 'published', set by the editor's "Save Draft" vs "Publish" buttons (AdminCategoriesFacade.saveDraft(publish)). - Local draft recovery + unsaved-changes guard: every
updateDraft()call persists the in-progress category tolocalStorageunderadmin-category-draft:<id>(via the existingLocalStorageService, same pattern as Project Editor autosave); the editor reloads that draft ahead of the saved value if present, and is cleared on save.adminCategoryDirtyGuard(mirrorsprojectEditorDirtyGuard) blocks navigation away from an unsaved edit withwindow.confirm. - Image: reuses the existing
MediaPickerComponent(same one used by Media Manager) rather than a free-text URL field. - Seed data:
AdminCategoriesLocalGatewayseeds its in-memory cache fromBackofficeDataService.loadCategories()(CategoryCardConfig, currently flat/no hierarchy) - same swappable-provider pattern asAdminProductsLocalGateway. - Not yet wired:
admin/products' category<select>still usesAdminProductsGateway.loadCategories()(its ownAdminProductCategoryOptionseed), notAdminCategoriesGateway- unifying them is Sprint 21 scope (docs/SPRINT-PLAN.md).
Sprint 21 - Product Management completion
- Categories now real:
AdminProductsLocalGatewayseeds its category dropdown fromAdminCategoriesLocalGateway.loadCategories()(Sprint 20) instead of rawBackofficeDataService.loadCategories()- productcategoryIdnow points at real admin-managed categories. - Archive/restore:
AdminProduct.archived(soft, distinct fromvisible). List has an "include archived" filter + per-row Archive/Restore action; archived products excluded by default (mirrors categories'deletedAt/restore pattern). - Barcode: added alongside
sku. - Variants: lightweight
AdminProductVariant[](name/price/quantity), edited asname|price|quantitylines (same textarea-parse convention asspecifications/attributes). Not a full options-matrix variant system - scoped to what the model/backend contract actually needs today. - Related products:
relatedProductIds: string[], checkbox picker in the editor sourced fromAdminProductFormComponent'sallProductsinput - which isAdminProductsFacade.products(), i.e. whatever page is currently loaded in the facade (usually primed by navigating from the list). Not a full catalog search; fine for the current mock-data scale, worth revisiting ifAdminProductsLocalGatewayis ever swapped for a real API with more than a page of products. - Gallery:
media.gallerynow built via the sharedMediaPickerComponent(add/remove thumbnails) instead of a raw URL textarea;media.images/media.videosunchanged (still textarea, out of this ticket's scope). - Preview: simple read-only line in the editor showing computed discounted price.
- Infinite scroll:
AdminProductsFacade.infiniteScrolltoggle - when on,loadMore()appends the next page toproducts()instead of replacing it; pagination UI swaps for a "Load more" button. Off by default (existing paginated behavior unchanged).
Known gaps / backend needs
- Dashboard metrics endpoint. Categories/Products counts are computed
client-side from
BackofficeDataService(itself mock/API-switchable viaBACKOFFICE_DATA_PROVIDER). A dedicated/builder/dashboard/summary-style endpoint would letAdminDashboardMetricsGatewayreturn 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.