feat: add admin dashboard summary
Quality / test (push) Canceled after 0s

Add seller-scoped dashboard aggregation and API, private financial summary DTOs, responsive admin metrics, and authenticated E2E coverage.
This commit is contained in:
2026-09-09 01:00:28 -04:00
parent 1d13c79a04
commit 455971431f
11 changed files with 177 additions and 1 deletions
+5
View File
@@ -16,6 +16,7 @@ import { registerPublicSearchRoutes } from '../modules/search/routes/public-sear
import { registerPublicSellerRoutes } from '../modules/sellers/routes/public-seller-routes.js';
import { registerAdminSaleRoutes } from '../modules/sales/routes/admin-sale-routes.js';
import { registerAdminAcquisitionRoutes } from '../modules/acquisitions/routes/admin-acquisition-routes.js';
import { registerAdminDashboardRoutes } from '../modules/dashboard/routes/admin-dashboard-routes.js';
export function buildApp(dependencies) {
const app = Fastify();
@@ -71,6 +72,10 @@ export function buildApp(dependencies) {
...dependencies,
prefix: '/api/v1/admin',
});
app.register(registerAdminDashboardRoutes, {
...dependencies,
prefix: '/api/v1/admin',
});
app.register(registerPublicSearchRoutes, {
...dependencies,
prefix: '/api/v1',
+8
View File
@@ -41,6 +41,7 @@ import { createSaleRepository } from '../modules/sales/repositories/sale-reposit
import { createSaleService } from '../modules/sales/services/sale-service.js';
import { createAcquisitionRepository } from '../modules/acquisitions/repositories/acquisition-repository.js';
import { createAcquisitionService } from '../modules/acquisitions/services/acquisition-service.js';
import { createDashboardService } from '../modules/dashboard/services/dashboard-service.js';
function parsePort(value) {
const port = Number.parseInt(value ?? '3000', 10);
@@ -123,6 +124,13 @@ async function startServer() {
listingService: createListingService({ listingRepository, inventoryImageRepository }),
saleService: createSaleService({ saleRepository }),
acquisitionService: createAcquisitionService({ acquisitionRepository }),
dashboardService: createDashboardService({
productService: createProductService({ productRepository }),
inventoryItemService: createInventoryItemService({ conditionRepository, inventoryItemRepository, productRepository }),
listingService: createListingService({ listingRepository, inventoryImageRepository }),
saleService: createSaleService({ saleRepository }),
acquisitionService: createAcquisitionService({ acquisitionRepository }),
}),
publicListingService: createPublicListingService({ listingRepository, publicImageService }),
productService: createProductService({ productRepository }),
publicProductService: createPublicProductService({ productRepository }),
@@ -0,0 +1,20 @@
import { toAdminDashboardDto } from '@pokemon-inventory/contracts/dashboard';
import { createApplicationError } from '../../../shared/errors/application-error.js';
export async function registerAdminDashboardRoutes(app, options) {
const {
authenticateAdmin = async () => { throw createApplicationError('UNAUTHORIZED', 'Authentication is required.', 401); },
dashboardService,
} = options;
app.addHook('preHandler', async (request) => {
const admin = await authenticateAdmin(request);
if (!admin?.sellerId) throw createApplicationError('UNAUTHORIZED', 'Authentication is required.', 401);
request.admin = admin;
});
app.get('/dashboard', async (request) => ({
data: toAdminDashboardDto(await dashboardService.get({ sellerId: request.admin.sellerId })),
}));
}
@@ -0,0 +1,38 @@
export function createDashboardService({ productService, inventoryItemService, listingService, saleService, acquisitionService }) {
return {
async get({ sellerId }) {
const [products, inventory, listings, sales, acquisitions] = await Promise.all([
productService.listBySellerId(sellerId),
inventoryItemService.listBySellerId(sellerId),
listingService.list({ sellerId }),
saleService.list({ sellerId }),
acquisitionService.list({ sellerId }),
]);
const activeListings = listings.filter(({ status }) => ['AVAILABLE', 'PENDING'].includes(status));
const completedSales = sales.filter(({ status }) => status !== 'VOIDED');
const total = (values, field) => values.reduce((sum, value) => sum + (value[field] ?? 0), 0);
return {
counts: {
products: products.length,
inventory: inventory.length,
activeInventory: inventory.filter(({ status }) => status === 'ACTIVE').length,
soldInventory: inventory.filter(({ status }) => status === 'SOLD').length,
availableListings: activeListings.length,
sales: completedSales.length,
acquisitions: acquisitions.length,
missingImages: listings.filter(({ imageCount }) => imageCount < 1).length,
},
financials: {
totalAcquisitionCostMinor: total(acquisitions, 'costBasisMinor'),
totalAskingValueMinor: total(activeListings, 'askingPriceMinor'),
totalNetSaleProceedsMinor: total(completedSales, 'netProceedsMinor'),
totalProfitMinor: total(completedSales, 'profitMinor'),
},
recentSales: completedSales.slice(0, 5),
recentAcquisitions: acquisitions.slice(0, 5),
};
},
};
}