diff --git a/docs/superpowers/plans/2026-08-15-admin-product-views-column.md b/docs/superpowers/plans/2026-08-15-admin-product-views-column.md new file mode 100644 index 0000000..e59ff63 --- /dev/null +++ b/docs/superpowers/plans/2026-08-15-admin-product-views-column.md @@ -0,0 +1,225 @@ +# 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`: + +```typescript +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;`): + +```typescript + 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): + +```typescript + 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: + +```typescript + 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: + +```typescript +export const ALL_PRODUCT_COLUMNS = ['sku', 'brand', 'price', 'stock', 'visibility', 'updated'] as const; +``` + +to: + +```typescript +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): + +```html + @if (isColumnVisible('visibility')) {