feat(ADM-018): completed feature
This commit is contained in:
95
project/src/modules/admin-stats/api/stats.routes.ts
Normal file
95
project/src/modules/admin-stats/api/stats.routes.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type pg from 'pg';
|
||||
import { z } from 'zod';
|
||||
import { requireRole, type Authenticate } from '../../../shared/auth.js';
|
||||
import { parseJson } from '../../../shared/http-input.js';
|
||||
|
||||
interface AdminStatsDeps {
|
||||
pool: pg.Pool;
|
||||
authenticate: Authenticate;
|
||||
}
|
||||
|
||||
export async function registerAdminStatsRoutes(
|
||||
app: FastifyInstance,
|
||||
deps: AdminStatsDeps,
|
||||
): Promise<void> {
|
||||
app.get('/admin/stats', async (request, reply) => {
|
||||
const user = await deps.authenticate(request);
|
||||
requireRole(user, 'admin');
|
||||
|
||||
const pool = deps.pool;
|
||||
|
||||
// All queries run in parallel for speed
|
||||
const [
|
||||
ordersToday,
|
||||
revenueToday,
|
||||
ordersByState,
|
||||
outOfStockVariants,
|
||||
totalProducts,
|
||||
newCustomersThisMonth,
|
||||
] = await Promise.all([
|
||||
// Orders today
|
||||
pool
|
||||
.query<{ count: string }>(
|
||||
`SELECT COUNT(*)::text AS count FROM orders_orders
|
||||
WHERE created_at >= CURRENT_DATE`,
|
||||
)
|
||||
.then((r) => parseInt(r.rows[0]?.count ?? '0', 10)),
|
||||
|
||||
// Revenue today (sum of PAID, PROCESSING, SHIPPED, DELIVERED orders)
|
||||
pool
|
||||
.query<{ total: string }>(
|
||||
`SELECT COALESCE(SUM(total_cents), 0)::text AS total FROM orders_orders
|
||||
WHERE created_at >= CURRENT_DATE
|
||||
AND state IN ('PAID','PROCESSING','SHIPPED','DELIVERED')`,
|
||||
)
|
||||
.then((r) => parseInt(r.rows[0]?.total ?? '0', 10)),
|
||||
|
||||
// Orders by state
|
||||
pool
|
||||
.query<{ state: string; count: string }>(
|
||||
`SELECT state, COUNT(*)::text AS count FROM orders_orders
|
||||
GROUP BY state ORDER BY count DESC`,
|
||||
)
|
||||
.then((r) =>
|
||||
Object.fromEntries(r.rows.map((row) => [row.state, parseInt(row.count, 10)])),
|
||||
),
|
||||
|
||||
// Out-of-stock variants
|
||||
pool
|
||||
.query<{ count: string }>(
|
||||
`SELECT COUNT(*)::text AS count
|
||||
FROM inventory_stock s
|
||||
WHERE s.available <= 0`,
|
||||
)
|
||||
.then((r) => parseInt(r.rows[0]?.count ?? '0', 10)),
|
||||
|
||||
// Total active products
|
||||
pool
|
||||
.query<{ count: string }>(
|
||||
`SELECT COUNT(*)::text AS count FROM catalog_products WHERE state = 'active'`,
|
||||
)
|
||||
.then((r) => parseInt(r.rows[0]?.count ?? '0', 10)),
|
||||
|
||||
// New customers this month
|
||||
pool
|
||||
.query<{ count: string }>(
|
||||
`SELECT COUNT(*)::text AS count FROM identity_users
|
||||
WHERE role = 'customer'
|
||||
AND created_at >= DATE_TRUNC('month', CURRENT_DATE)`,
|
||||
)
|
||||
.then((r) => parseInt(r.rows[0]?.count ?? '0', 10)),
|
||||
]);
|
||||
|
||||
return reply.send({
|
||||
ordersToday,
|
||||
revenueTodayCents: revenueToday,
|
||||
revenueTodayFormatted: `€${(revenueToday / 100).toFixed(2)}`,
|
||||
ordersByState,
|
||||
outOfStockVariants,
|
||||
totalActiveProducts: totalProducts,
|
||||
newCustomersThisMonth,
|
||||
generatedAt: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user