diff --git a/src/routes/admin.ts b/src/routes/admin.ts index fda4abc..c03585e 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -5,6 +5,14 @@ import type { Context, Next } from "@oak/oak"; export const adminRouter = new Router({ prefix: "/api/admin" }); +// Helper to stringify with BigInt support - returns a string for ctx.response.body +function jsonResponse(obj: unknown): string { + return JSON.stringify(obj, (_key, value) => { + if (typeof value === 'bigint') return Number(value); + return value; + }); +} + // Super Admin emails (hardcoded for security) const SUPER_ADMINS = [ "admin@kronos-soulution.de", @@ -33,99 +41,126 @@ async function requireSuperAdmin(ctx: Context, next: Next): Promise { // Dashboard stats adminRouter.get("/dashboard", requireSuperAdmin, async (ctx) => { - const stats = await queryOne<{ - org_count: number; - user_count: number; - order_count: number; - timesheet_count: number; - }>( - `SELECT - (SELECT COUNT(*) FROM organizations) as org_count, - (SELECT COUNT(*) FROM users WHERE active = true) as user_count, - (SELECT COUNT(*) FROM orders) as order_count, - (SELECT COUNT(*) FROM timesheets) as timesheet_count` - ); - - // Recent orgs - const recentOrgs = await query<{ id: string; name: string; slug: string; created_at: Date }>( - `SELECT id, name, slug, created_at - FROM organizations - ORDER BY created_at DESC - LIMIT 10` - ); - - ctx.response.body = { stats, recentOrgs }; + try { + // Use ::text to avoid BigInt issues + const rawStats = await queryOne( + `SELECT + (SELECT COUNT(*) FROM organizations)::text as org_count, + (SELECT COUNT(*) FROM users WHERE active = true)::text as user_count, + (SELECT COUNT(*) FROM orders)::text as order_count, + (SELECT COUNT(*) FROM timesheets)::text as timesheet_count` + ); + + console.log("rawStats:", rawStats); + + const stats = { + org_count: Number(rawStats?.org_count || 0), + user_count: Number(rawStats?.user_count || 0), + order_count: Number(rawStats?.order_count || 0), + timesheet_count: Number(rawStats?.timesheet_count || 0) + }; + + console.log("stats:", stats); + + // Recent orgs - also cast to text to avoid BigInt + const rawOrgs = await query( + `SELECT id::text as id, name, slug, to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') as created_at + FROM organizations + ORDER BY created_at DESC + LIMIT 10` + ); + + console.log("rawOrgs:", rawOrgs); + + const recentOrgs = rawOrgs.map((o: Record) => ({ + id: Number(o.id), + name: String(o.name), + slug: String(o.slug), + created_at: String(o.created_at) + })); + + console.log("recentOrgs:", recentOrgs); + + const responseBody = { stats, recentOrgs }; + + // Stringify manually and set as text response to avoid Oak's JSON.stringify + ctx.response.type = "application/json"; + ctx.response.body = JSON.stringify(responseBody); + } catch (err) { + console.error("Dashboard error:", err); + throw err; + } }); // List all organizations adminRouter.get("/organizations", requireSuperAdmin, async (ctx) => { - const orgs = await query<{ - id: string; - name: string; - slug: string; - settings: Record; - created_at: Date; - user_count: number; - order_count: number; - }>( - `SELECT o.*, - (SELECT COUNT(*) FROM users WHERE org_id = o.id AND active = true) as user_count, - (SELECT COUNT(*) FROM orders WHERE org_id = o.id) as order_count + const rawOrgs = await query( + `SELECT o.id::text, o.name, o.slug, o.status, o.plan, o.max_users::text, o.created_at::text, + (SELECT COUNT(*)::text FROM users WHERE org_id = o.id AND active = true) as user_count, + (SELECT COUNT(*)::text FROM orders WHERE org_id = o.id) as order_count FROM organizations o ORDER BY o.created_at DESC` ); - ctx.response.body = { organizations: orgs }; + const organizations = rawOrgs.map((o: Record) => ({ + ...o, + id: Number(o.id), + max_users: Number(o.max_users || 10), + user_count: Number(o.user_count || 0), + order_count: Number(o.order_count || 0) + })); + + ctx.response.body = { organizations }; }); // Get single organization details adminRouter.get("/organizations/:id", requireSuperAdmin, async (ctx) => { const orgId = ctx.params.id; - const org = await queryOne<{ - id: string; - name: string; - slug: string; - settings: Record; - created_at: Date; - }>( - "SELECT * FROM organizations WHERE id = $1", + const rawOrg = await queryOne( + `SELECT id::text, name, slug, status, plan, max_users::text, created_at::text + FROM organizations WHERE id = $1`, [orgId] ); - if (!org) { + if (!rawOrg) { throw new AppError("Organisation nicht gefunden", 404); } + const org = { + ...rawOrg, + id: Number(rawOrg.id), + max_users: Number(rawOrg.max_users || 10) + }; + // Get users - const users = await query<{ - id: string; - email: string; - role: string; - first_name: string; - last_name: string; - active: boolean; - last_login: Date; - }>( - `SELECT id, email, role, first_name, last_name, active, last_login + const rawUsers = await query( + `SELECT id::text, email, role, first_name, last_name, active, last_login::text FROM users WHERE org_id = $1 ORDER BY role, last_name`, [orgId] ); + const users = rawUsers.map((u: Record) => ({ + ...u, + id: Number(u.id) + })); + // Get stats - const stats = await queryOne<{ - order_count: number; - timesheet_count: number; - enabled_modules: number; - }>( + const rawStats = await queryOne( `SELECT - (SELECT COUNT(*) FROM orders WHERE org_id = $1) as order_count, - (SELECT COUNT(*) FROM timesheets t JOIN users u ON t.user_id = u.id WHERE u.org_id = $1) as timesheet_count, - (SELECT COUNT(*) FROM organization_modules WHERE org_id = $1 AND enabled = true) as enabled_modules`, + (SELECT COUNT(*)::text FROM orders WHERE org_id = $1) as order_count, + (SELECT COUNT(*)::text FROM timesheets t JOIN users u ON t.user_id = u.id WHERE u.org_id = $1) as timesheet_count, + (SELECT COUNT(*)::text FROM organization_modules WHERE org_id = $1 AND enabled = true) as enabled_modules`, [orgId] ); + const stats = { + order_count: Number(rawStats?.order_count || 0), + timesheet_count: Number(rawStats?.timesheet_count || 0), + enabled_modules: Number(rawStats?.enabled_modules || 0) + }; + ctx.response.body = { organization: org, users, stats }; });