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>
227 lines
13 KiB
Markdown
227 lines
13 KiB
Markdown
# 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 `<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 via `AdminCategoriesFacade.reorder()` which just rewrites `order`.
|
|
- **Delete/restore**: soft delete (`deletedAt` timestamp). Blocked
|
|
client-side (`facade.canDelete()`) if the category has children or
|
|
`itemsCount > 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 to `localStorage` under
|
|
`admin-category-draft:<id>` (via the existing `LocalStorageService`,
|
|
same pattern as Project Editor autosave); the editor reloads that draft
|
|
ahead of the saved value if present, and is cleared on save.
|
|
`adminCategoryDirtyGuard` (mirrors `projectEditorDirtyGuard`) blocks
|
|
navigation away from an unsaved edit with `window.confirm`.
|
|
- **Image**: reuses the existing `MediaPickerComponent` (same one used by
|
|
Media Manager) rather than a free-text URL field.
|
|
- **Seed data**: `AdminCategoriesLocalGateway` seeds its in-memory cache from
|
|
`BackofficeDataService.loadCategories()` (`CategoryCardConfig`, currently
|
|
flat/no hierarchy) - same swappable-provider pattern as
|
|
`AdminProductsLocalGateway`.
|
|
- **Not yet wired**: `admin/products`' category `<select>` still uses
|
|
`AdminProductsGateway.loadCategories()` (its own `AdminProductCategoryOption`
|
|
seed), not `AdminCategoriesGateway` - unifying them is Sprint 21 scope
|
|
(`docs/SPRINT-PLAN.md`).
|
|
|
|
## Sprint 21 - Product Management completion
|
|
|
|
- **Categories now real**: `AdminProductsLocalGateway` seeds its category dropdown from `AdminCategoriesLocalGateway.loadCategories()` (Sprint 20) instead of raw `BackofficeDataService.loadCategories()` - product `categoryId` now points at real admin-managed categories.
|
|
- **Archive/restore**: `AdminProduct.archived` (soft, distinct from `visible`). 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 as `name|price|quantity` lines (same textarea-parse convention as `specifications`/`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 from `AdminProductFormComponent`'s `allProducts` input - which is `AdminProductsFacade.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 if `AdminProductsLocalGateway` is ever swapped for a real API with more than a page of products.
|
|
- **Gallery**: `media.gallery` now built via the shared `MediaPickerComponent` (add/remove thumbnails) instead of a raw URL textarea; `media.images`/`media.videos` unchanged (still textarea, out of this ticket's scope).
|
|
- **Preview**: simple read-only line in the editor showing computed discounted price.
|
|
- **Infinite scroll**: `AdminProductsFacade.infiniteScroll` toggle - when on, `loadMore()` appends the next page to `products()` 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 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.
|