feat: admin product views column (always 0 until backend tracks it)
Some checks failed
Architecture Governance / architecture (push) Has been cancelled

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-15 16:18:20 +04:00
parent 3056be53db
commit 0d1d468307
11 changed files with 88 additions and 2 deletions

View File

@@ -98,6 +98,7 @@
@if (isColumnVisible('price')) { <th scope="col">{{ 'backoffice.price' | translate }}</th> }
@if (isColumnVisible('stock')) { <th scope="col">{{ 'adminProducts.stockStatus' | translate }}</th> }
@if (isColumnVisible('visibility')) { <th scope="col">{{ 'adminProducts.visibility' | translate }}</th> }
@if (isColumnVisible('visits')) { <th scope="col">{{ 'adminProducts.views' | translate }}</th> }
<th scope="col">{{ 'adminProducts.healthColumn' | translate }}</th>
<th scope="col">{{ 'adminProducts.actions' | translate }}</th>
</tr>
@@ -124,6 +125,7 @@
</app-badge>
</td>
}
@if (isColumnVisible('visits')) { <td>{{ product.visits }}</td> }
<td class="health-cell"><app-product-health-widget [items]="healthItems(product)" [completionPercent]="health(product).completionPercent" [compact]="true" /></td>
<td class="actions">
<app-button variant="secondary" size="sm" (click)="edit.emit(product.id)">{{ 'adminProducts.edit' | translate }}</app-button>

View File

@@ -36,7 +36,7 @@ const VIEW_MODE_KEY = 'admin-products:view-mode';
const DENSITY_KEY = 'admin-products:density';
const COLUMNS_KEY = 'admin-products:visible-columns';
export const ALL_PRODUCT_COLUMNS = ['sku', 'brand', 'price', 'stock', 'visibility', 'updated'] as const;
export const ALL_PRODUCT_COLUMNS = ['sku', 'brand', 'price', 'stock', 'visibility', 'updated', 'visits'] as const;
export type AdminProductColumn = typeof ALL_PRODUCT_COLUMNS[number];
@Injectable({ providedIn: 'root' })

View File

@@ -101,6 +101,7 @@ export interface AdminProduct {
specifications: AdminProductSpecification[];
attributes: AdminProductAttribute[];
variantAttributes: AdminProductVariantAttributeDef[];
visits: number;
variants: AdminProductVariant[];
relatedProductIds: string[];
translations: Record<string, AdminProductTranslation>;

View File

@@ -20,6 +20,7 @@ export class AdminProductsFormFactory {
discount: 0,
currency: 'RUB',
quantity: 0,
visits: 0,
stockStatus: 'in_stock',
availability: 'in_stock',
shortDescription: '',

View File

@@ -0,0 +1,64 @@
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { AdminProductsLocalGateway } from './admin-products-local.gateway';
import { BackofficeDataService } from '../../../../core/backoffice/backoffice-data.service';
import { AdminCategoriesLocalGateway } from '../../categories/services/admin-categories-local.gateway';
import { ProductCardConfig } from '../../../../shared/models/ui';
import { AdminProductListFilters } from '../models/admin-product.model';
function makeProductCard(id: string): ProductCardConfig {
return {
id,
sku: `SKU-${id}`,
title: `Product ${id}`,
imageUrl: '/image.png',
price: { amount: 1000, currency: 'RUB' },
stockStatus: 'in_stock',
};
}
function makeFilters(): AdminProductListFilters {
return {
search: '',
categoryId: null,
visibility: 'all',
stock: 'all',
includeArchived: true,
sort: 'title',
page: 1,
pageSize: 50,
};
}
describe('AdminProductsLocalGateway visits field', () => {
let gateway: AdminProductsLocalGateway;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: BackofficeDataService, useValue: { loadProducts: () => of([makeProductCard('p1'), makeProductCard('p2')]) } },
{ provide: AdminCategoriesLocalGateway, useValue: { loadCategories: () => of([{ id: 'cat-001', title: 'Category' }]) } },
],
});
gateway = TestBed.inject(AdminProductsLocalGateway);
});
it('defaults visits to 0 on every loaded product', (done) => {
gateway.loadProducts(makeFilters()).subscribe(result => {
expect(result.items.length).toBe(2);
expect(result.items.every(product => product.visits === 0)).toBe(true);
done();
});
});
it('resets visits to 0 on a duplicated product', (done) => {
gateway.loadProducts(makeFilters()).subscribe(result => {
const source = result.items[0];
gateway.duplicateProduct(source.id).subscribe(duplicated => {
expect(duplicated).not.toBeNull();
expect(duplicated!.visits).toBe(0);
done();
});
});
});
});

View File

@@ -86,6 +86,7 @@ export class AdminProductsLocalGateway implements AdminProductsGateway {
sku: `${source.sku}-COPY`,
slug: `${source.slug}-copy-${Date.now()}`,
name: `${source.name} Copy`,
visits: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
@@ -153,6 +154,7 @@ export class AdminProductsLocalGateway implements AdminProductsGateway {
: 0,
currency: product.price.currency,
quantity: product.stockStatus === 'out_of_stock' ? 0 : product.stockStatus === 'low_stock' ? 3 : 25,
visits: 0,
stockStatus: product.stockStatus ?? 'in_stock',
availability: product.stockStatus ?? 'in_stock',
shortDescription: product.subtitle ?? '',