96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
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(),
|
|
});
|
|
});
|
|
}
|