docs: implementation plan for admin product views column (item 1)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
225
docs/superpowers/plans/2026-08-15-admin-product-views-column.md
Normal file
225
docs/superpowers/plans/2026-08-15-admin-product-views-column.md
Normal file
@@ -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')) { <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 `}`):
|
||||||
|
|
||||||
|
```html
|
||||||
|
@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):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
stockStatus: string;
|
||||||
|
views: string;
|
||||||
|
```
|
||||||
|
```typescript
|
||||||
|
column_updated: string;
|
||||||
|
column_visits: string;
|
||||||
|
```
|
||||||
|
|
||||||
|
In `src/app/i18n/en.ts`, `adminProducts` block:
|
||||||
|
```typescript
|
||||||
|
stockStatus: 'Stock status',
|
||||||
|
views: 'Views',
|
||||||
|
```
|
||||||
|
```typescript
|
||||||
|
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):
|
||||||
|
```typescript
|
||||||
|
views: 'Просмотры',
|
||||||
|
```
|
||||||
|
```typescript
|
||||||
|
column_visits: 'Просмотры',
|
||||||
|
```
|
||||||
|
|
||||||
|
In `src/app/i18n/hy.ts`, `adminProducts` block:
|
||||||
|
```typescript
|
||||||
|
views: 'Դիտումներ',
|
||||||
|
```
|
||||||
|
```typescript
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### 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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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)"
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user