Files
marketplaces/docs/superpowers/plans/2026-08-15-admin-product-views-column.md
2026-08-15 16:11:09 +04:00

10 KiB

Admin Product Views Column Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Show a real (currently-zero) per-product view count as a toggleable column in Admin Products list, and document the backend gap that keeps it at zero today.

Architecture: Add visits: number to the AdminProduct model, default it to 0 everywhere the mock gateway constructs an AdminProduct, add 'visits' to the existing toggleable-column system (ALL_PRODUCT_COLUMNS), render it in the table view using the established isColumnVisible() pattern.

Tech Stack: Angular signals, existing LocalStorageService-backed column-visibility persistence (already built, not touched).

Global Constraints

  • Never fabricate view numbers — the mock gateway has no real tracking source, so visits must default to 0, not a random/seeded number.
  • Table view only — no grid-view or product-detail-page display (out of scope per design doc).
  • Follow the existing isColumnVisible('stock')-style pattern exactly — no new column-visibility mechanism.

Task 1: visits field, column, and backend doc ask

Files:

  • Modify: src/app/features/admin/products/models/admin-product.model.ts:81-121 (add field)
  • Modify: src/app/features/admin/products/services/admin-products-local.gateway.ts:82-94,133-166 (default the field)
  • Modify: src/app/features/admin/products/facade/admin-products.facade.ts:39 (add to column list)
  • Modify: src/app/features/admin/products/components/admin-products-list.component.html:96-112 (render column + header)
  • Modify: src/app/i18n/en.ts:1647,1670, src/app/i18n/ru.ts:1642, src/app/i18n/hy.ts:1642, src/app/i18n/translations.ts:1655 (i18n keys)
  • Modify: BACKEND-API-REFERENCE.md (new §12.10 ask)
  • Test: src/app/features/admin/products/services/admin-products-local.gateway.spec.ts (new)

Interfaces:

  • Produces: AdminProduct.visits: number

  • Produces: ALL_PRODUCT_COLUMNS includes 'visits' (so AdminProductColumn union includes 'visits')

  • Step 1: Write the failing test

Create src/app/features/admin/products/services/admin-products-local.gateway.spec.ts:

import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { AdminProductsLocalGateway } from './admin-products-local.gateway';

describe('AdminProductsLocalGateway visits field', () => {
  let gateway: AdminProductsLocalGateway;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideHttpClient(), provideHttpClientTesting()],
    });
    gateway = TestBed.inject(AdminProductsLocalGateway);
  });

  it('defaults visits to 0 on every loaded product', (done) => {
    gateway.loadProducts({ search: '', categoryId: 'all', visibility: 'all', stockStatus: 'all', page: 1, pageSize: 50 }).subscribe(result => {
      expect(result.items.length).toBeGreaterThan(0);
      expect(result.items.every(product => product.visits === 0)).toBe(true);
      done();
    });
  });

  it('resets visits to 0 on a duplicated product, even if the source had a nonzero count', (done) => {
    gateway.loadProducts({ search: '', categoryId: 'all', visibility: 'all', stockStatus: 'all', page: 1, pageSize: 50 }).subscribe(result => {
      const source = result.items[0];
      gateway.duplicateProduct(source.id).subscribe(duplicated => {
        expect(duplicated).not.toBeNull();
        expect(duplicated!.visits).toBe(0);
        done();
      });
    });
  });
});

Note: read src/app/features/admin/products/models/admin-product.model.ts for the exact AdminProductListFilters shape before writing the test's filter object — if the field names above (categoryId, visibility, stockStatus, page, pageSize) don't match exactly, use the real ones; don't guess.

  • Step 2: Run test to verify it fails

Run: npm run test -- --include='**/admin-products-local.gateway.spec.ts' Expected: FAIL — Property 'visits' does not exist on type 'AdminProduct' (TS compile error surfaces as a Karma failure).

  • Step 3: Add the field to the model

In src/app/features/admin/products/models/admin-product.model.ts, add to the AdminProduct interface (next to quantity: number;):

  quantity: number;
  visits: number;
  • Step 4: Default it in the mock gateway

In src/app/features/admin/products/services/admin-products-local.gateway.ts, in toAdminProduct() (around line 155, next to the quantity line):

      quantity: product.stockStatus === 'out_of_stock' ? 0 : product.stockStatus === 'low_stock' ? 3 : 25,
      visits: 0,

In duplicateProduct() (around line 82-91), add visits: 0 to the override object so a duplicate never inherits the source's count via the ...source spread:

    const duplicated: AdminProduct = {
      ...source,
      archived: false,
      id: `${source.id}-copy-${Date.now()}`,
      sku: `${source.sku}-COPY`,
      slug: `${source.slug}-copy-${Date.now()}`,
      name: `${source.name} Copy`,
      visits: 0,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    };
  • Step 5: Run test to verify it passes

Run: npm run test -- --include='**/admin-products-local.gateway.spec.ts' Expected: PASS (2/2)

  • Step 6: Add the column

In src/app/features/admin/products/facade/admin-products.facade.ts:39, change:

export const ALL_PRODUCT_COLUMNS = ['sku', 'brand', 'price', 'stock', 'visibility', 'updated'] as const;

to:

export const ALL_PRODUCT_COLUMNS = ['sku', 'brand', 'price', 'stock', 'visibility', 'updated', 'visits'] as const;

In src/app/features/admin/products/components/admin-products-list.component.html, add a header cell after the visibility header (around line 100):

          @if (isColumnVisible('visibility')) { <th scope="col">{{ 'adminProducts.visibility' | translate }}</th> }
          @if (isColumnVisible('visits')) { <th scope="col">{{ 'adminProducts.views' | translate }}</th> }

And a matching body cell after the visibility cell (around line 126, right after its closing }):

            @if (isColumnVisible('visits')) { <td>{{ product.visits }}</td> }

The column-picker panel (admin-products-list.component.html:50-59) needs no template change — it already iterates allColumns generically and looks up adminProducts.column_<name>, so it auto-picks up 'visits' once the i18n key exists (Step 7).

  • Step 7: Add i18n keys

In src/app/i18n/translations.ts, in the adminProducts interface block, add two lines (next to stockStatus: string; and near the other column_* entries):

    stockStatus: string;
    views: string;
    column_updated: string;
    column_visits: string;

In src/app/i18n/en.ts, adminProducts block:

    stockStatus: 'Stock status',
    views: 'Views',
    column_updated: 'Last updated',
    column_visits: 'Views',

In src/app/i18n/ru.ts, adminProducts block (next to its stockStatus: line and its column_updated: line — read the file first to find them, they're at different line numbers than en.ts):

    views: 'Просмотры',
    column_visits: 'Просмотры',

In src/app/i18n/hy.ts, adminProducts block:

    views: 'Դիտումներ',
    column_visits: 'Դիտումներ',

(For ru.ts/hy.ts: read the file first, find the exact existing stockStatus:/column_updated: lines in the adminProducts block — there may be more than one column_updated: in the file for a different admin domain, only edit the one inside adminProducts, at the location already found: ru.ts:1642 area, hy.ts:1642 area.)

  • Step 8: Run full verification

Run: npx tsc --noEmit -p tsconfig.json Expected: no errors.

Run: npx ng build --configuration development Expected: build succeeds.

Run: npm run test -- --include='**/admin-products-local.gateway.spec.ts' Expected: PASS (2/2).

  • Step 9: Document the backend gap

In BACKEND-API-REFERENCE.md, after the existing §12.8 section (search for ### 12.8 Admin purchase notifications depend on Orders CRUD being real — it currently ends right before ### 12.9 Trending search terms), insert a new section, and renumber 12.9 to 12.10:

### 12.9 Admin product view counts

**Gap:** Admin Products (§8) runs on a fully separate mock domain from the storefront's live catalog — `AdminProduct.visits` is a new field added to support a "Views" column in Admin Products, but the mock gateway always defaults it to `0` because there is no real tracking source available to the admin domain today. This is unrelated to the storefront's `Item.visits` field (§6, `/items/{id}`), which is live-wired but never displayed anywhere in the UI.

**Ask:** two options, not mutually exclusive:
1. Once admin Products gets a real backend (§10 step 4), include a per-product view/visit count in the response.
2. Bridge `AdminProduct.visits` to the storefront's already-live `Item.visits` by product id, if a unified product identity exists between the storefront and admin domains — smaller change than building new tracking infrastructure.

### 12.10 Trending search terms

(The existing body text of the old ### 12.9 Trending search terms section stays exactly as-is below the renumbered heading — only the heading number changes, from 12.9 to 12.10.)

  • Step 10: Commit
git add src/app/features/admin/products/models/admin-product.model.ts src/app/features/admin/products/services/admin-products-local.gateway.ts src/app/features/admin/products/services/admin-products-local.gateway.spec.ts src/app/features/admin/products/facade/admin-products.facade.ts src/app/features/admin/products/components/admin-products-list.component.html src/app/i18n/en.ts src/app/i18n/ru.ts src/app/i18n/hy.ts src/app/i18n/translations.ts BACKEND-API-REFERENCE.md
git commit -m "feat: admin product views column (always 0 until backend tracks it)"