feat: Complete rewrite as Next.js admin dashboard
Some checks failed
Build and Push Docker Image / test (push) Failing after 34s
Build and Push Docker Image / build (push) Has been skipped

Major transformation from FastAPI telemetry service to Next.js admin dashboard:

- Next.js 15 App Router with TypeScript
- Prisma ORM with PostgreSQL (same schema, new client)
- TanStack Query for data fetching
- Tailwind CSS + shadcn/ui components
- Admin dashboard with:
  - Dashboard stats overview
  - Customer management (list, detail, edit)
  - Order management (list, create, detail, logs)
  - Server monitoring (grid view)
  - Subscription management

Pages implemented:
- /admin (dashboard)
- /admin/customers (list + [id] detail)
- /admin/orders (list + [id] detail with SSE logs)
- /admin/servers (grid view)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-06 12:35:01 +01:00
parent 02fc18f009
commit a79b79efd2
85 changed files with 19070 additions and 1869 deletions

View File

@@ -0,0 +1,150 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { UserStatus } from '@prisma/client'
/**
* GET /api/v1/admin/customers/[id]
* Get customer details with orders and subscriptions
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session || session.user.userType !== 'staff') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id: customerId } = await params
const customer = await prisma.user.findUnique({
where: { id: customerId },
include: {
subscriptions: {
orderBy: { createdAt: 'desc' },
},
orders: {
orderBy: { createdAt: 'desc' },
include: {
_count: {
select: { provisioningLogs: true },
},
},
},
tokenUsage: {
orderBy: { createdAt: 'desc' },
take: 100,
},
_count: {
select: {
orders: true,
subscriptions: true,
tokenUsage: true,
},
},
},
})
if (!customer) {
return NextResponse.json({ error: 'Customer not found' }, { status: 404 })
}
// Calculate total token usage
const totalTokensUsed = customer.tokenUsage.reduce(
(acc, usage) => acc + usage.tokensInput + usage.tokensOutput,
0
)
// Get current subscription's token limit
const currentSubscription = customer.subscriptions[0]
const tokenLimit = currentSubscription?.tokenLimit || 0
return NextResponse.json({
...customer,
totalTokensUsed,
tokenLimit,
})
} catch (error) {
console.error('Error getting customer:', error)
return NextResponse.json(
{ error: 'Failed to get customer' },
{ status: 500 }
)
}
}
/**
* PATCH /api/v1/admin/customers/[id]
* Update customer details
*/
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session || session.user.userType !== 'staff') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id: customerId } = await params
const body = await request.json()
// Validate customer exists
const existingCustomer = await prisma.user.findUnique({
where: { id: customerId },
})
if (!existingCustomer) {
return NextResponse.json({ error: 'Customer not found' }, { status: 404 })
}
// Build update data
const updateData: {
name?: string
company?: string
status?: UserStatus
} = {}
if (body.name !== undefined) {
updateData.name = body.name
}
if (body.company !== undefined) {
updateData.company = body.company
}
if (body.status !== undefined) {
updateData.status = body.status as UserStatus
}
const customer = await prisma.user.update({
where: { id: customerId },
data: updateData,
include: {
subscriptions: {
orderBy: { createdAt: 'desc' },
take: 1,
},
_count: {
select: {
orders: true,
subscriptions: true,
},
},
},
})
return NextResponse.json(customer)
} catch (error) {
console.error('Error updating customer:', error)
return NextResponse.json(
{ error: 'Failed to update customer' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,88 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { UserStatus, Prisma } from '@prisma/client'
/**
* GET /api/v1/admin/customers
* List all customers with optional filters
*/
export async function GET(request: NextRequest) {
try {
const session = await auth()
if (!session || session.user.userType !== 'staff') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const searchParams = request.nextUrl.searchParams
const status = searchParams.get('status') as UserStatus | null
const search = searchParams.get('search')
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '50')
const where: Prisma.UserWhereInput = {}
if (status) {
where.status = status
}
if (search) {
where.OR = [
{ email: { contains: search, mode: 'insensitive' } },
{ name: { contains: search, mode: 'insensitive' } },
{ company: { contains: search, mode: 'insensitive' } },
]
}
const [customers, total] = await Promise.all([
prisma.user.findMany({
where,
select: {
id: true,
email: true,
name: true,
company: true,
status: true,
createdAt: true,
subscriptions: {
select: {
id: true,
plan: true,
tier: true,
status: true,
},
take: 1,
orderBy: { createdAt: 'desc' },
},
_count: {
select: {
orders: true,
subscriptions: true,
},
},
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit,
}),
prisma.user.count({ where }),
])
return NextResponse.json({
customers,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
})
} catch (error) {
console.error('Error listing customers:', error)
return NextResponse.json(
{ error: 'Failed to list customers' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,148 @@
import { NextRequest } from 'next/server'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { OrderStatus } from '@prisma/client'
/**
* GET /api/v1/admin/orders/[id]/logs/stream
* Stream provisioning logs via Server-Sent Events
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session || session.user.userType !== 'staff') {
return new Response('Unauthorized', { status: 401 })
}
const { id: orderId } = await params
// Verify order exists
const order = await prisma.order.findUnique({
where: { id: orderId },
select: { id: true, status: true },
})
if (!order) {
return new Response('Order not found', { status: 404 })
}
// Create SSE response
const encoder = new TextEncoder()
let lastLogId: string | null = null
let isActive = true
const stream = new ReadableStream({
async start(controller) {
// Send initial connection message
controller.enqueue(
encoder.encode(`event: connected\ndata: ${JSON.stringify({ orderId })}\n\n`)
)
// Poll for new logs
const poll = async () => {
if (!isActive) return
try {
// Get current order status
const currentOrder = await prisma.order.findUnique({
where: { id: orderId },
select: { status: true },
})
if (!currentOrder) {
controller.enqueue(
encoder.encode(`event: error\ndata: ${JSON.stringify({ error: 'Order not found' })}\n\n`)
)
controller.close()
return
}
// Build query for new logs
const query: Parameters<typeof prisma.provisioningLog.findMany>[0] = {
where: {
orderId,
...(lastLogId ? { id: { gt: lastLogId } } : {}),
},
orderBy: { timestamp: 'asc' as const },
take: 50,
}
const newLogs = await prisma.provisioningLog.findMany(query)
if (newLogs.length > 0) {
// Update last seen log ID
lastLogId = newLogs[newLogs.length - 1].id
// Send each log as an event
for (const log of newLogs) {
controller.enqueue(
encoder.encode(`event: log\ndata: ${JSON.stringify({
id: log.id,
level: log.level,
step: log.step,
message: log.message,
timestamp: log.timestamp.toISOString(),
})}\n\n`)
)
}
}
// Send status update
controller.enqueue(
encoder.encode(`event: status\ndata: ${JSON.stringify({ status: currentOrder.status })}\n\n`)
)
// Check if provisioning is complete
const terminalStatuses: OrderStatus[] = [
OrderStatus.FULFILLED,
OrderStatus.EMAIL_CONFIGURED,
OrderStatus.FAILED,
]
if (terminalStatuses.includes(currentOrder.status)) {
controller.enqueue(
encoder.encode(`event: complete\ndata: ${JSON.stringify({
status: currentOrder.status,
success: currentOrder.status !== OrderStatus.FAILED
})}\n\n`)
)
controller.close()
return
}
// Continue polling if still provisioning
setTimeout(poll, 2000) // Poll every 2 seconds
} catch (err) {
console.error('SSE polling error:', err)
controller.enqueue(
encoder.encode(`event: error\ndata: ${JSON.stringify({ error: 'Polling error' })}\n\n`)
)
controller.close()
}
}
// Start polling
poll()
},
cancel() {
isActive = false
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // Disable nginx buffering
},
})
} catch (error) {
console.error('Error setting up SSE stream:', error)
return new Response('Internal Server Error', { status: 500 })
}
}

View File

@@ -0,0 +1,68 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { OrderStatus } from '@prisma/client'
import { jobService } from '@/lib/services/job-service'
/**
* POST /api/v1/admin/orders/[id]/provision
* Trigger provisioning for an order
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session || session.user.userType !== 'staff') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id: orderId } = await params
// Check if order exists and is ready for provisioning
const order = await prisma.order.findUnique({
where: { id: orderId },
})
if (!order) {
return NextResponse.json({ error: 'Order not found' }, { status: 404 })
}
// Validate order status - can only provision from DNS_READY or FAILED
const validStatuses: OrderStatus[] = [OrderStatus.DNS_READY, OrderStatus.FAILED]
if (!validStatuses.includes(order.status)) {
return NextResponse.json(
{
error: `Cannot provision order in status ${order.status}. Must be DNS_READY or FAILED.`,
},
{ status: 400 }
)
}
// Validate server credentials
if (!order.serverIp || !order.serverPasswordEncrypted) {
return NextResponse.json(
{ error: 'Server credentials not configured' },
{ status: 400 }
)
}
// Create provisioning job
const result = await jobService.createJobForOrder(orderId)
const { jobId } = JSON.parse(result)
return NextResponse.json({
success: true,
message: 'Provisioning job created',
jobId,
})
} catch (error) {
console.error('Error triggering provisioning:', error)
return NextResponse.json(
{ error: 'Failed to trigger provisioning' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,175 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { OrderStatus } from '@prisma/client'
import crypto from 'crypto'
/**
* GET /api/v1/admin/orders/[id]
* Get order details
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session || session.user.userType !== 'staff') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id: orderId } = await params
const order = await prisma.order.findUnique({
where: { id: orderId },
include: {
user: {
select: {
id: true,
name: true,
email: true,
company: true,
},
},
provisioningLogs: {
orderBy: { timestamp: 'desc' },
take: 100,
},
jobs: {
orderBy: { createdAt: 'desc' },
take: 5,
select: {
id: true,
status: true,
attempt: true,
maxAttempts: true,
createdAt: true,
completedAt: true,
error: true,
},
},
},
})
if (!order) {
return NextResponse.json({ error: 'Order not found' }, { status: 404 })
}
return NextResponse.json(order)
} catch (error) {
console.error('Error getting order:', error)
return NextResponse.json(
{ error: 'Failed to get order' },
{ status: 500 }
)
}
}
/**
* PATCH /api/v1/admin/orders/[id]
* Update order (status, server credentials, etc.)
*/
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session || session.user.userType !== 'staff') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id: orderId } = await params
const body = await request.json()
// Find existing order
const existingOrder = await prisma.order.findUnique({
where: { id: orderId },
})
if (!existingOrder) {
return NextResponse.json({ error: 'Order not found' }, { status: 404 })
}
// Build update data
const updateData: {
status?: OrderStatus
serverIp?: string
serverPasswordEncrypted?: string
sshPort?: number
serverReadyAt?: Date
failureReason?: string
} = {}
// Handle status update
if (body.status) {
updateData.status = body.status as OrderStatus
}
// Handle server credentials
if (body.serverIp) {
updateData.serverIp = body.serverIp
}
if (body.serverPassword) {
// Encrypt the password before storing
// TODO: Use proper encryption with environment-based key
const encrypted = encryptPassword(body.serverPassword)
updateData.serverPasswordEncrypted = encrypted
}
if (body.sshPort) {
updateData.sshPort = body.sshPort
}
// If server credentials are being set and status is AWAITING_SERVER, move to SERVER_READY
if (
(body.serverIp || body.serverPassword) &&
existingOrder.status === OrderStatus.AWAITING_SERVER
) {
updateData.status = OrderStatus.SERVER_READY
updateData.serverReadyAt = new Date()
}
// Update order
const order = await prisma.order.update({
where: { id: orderId },
data: updateData,
include: {
user: {
select: {
id: true,
name: true,
email: true,
},
},
},
})
return NextResponse.json(order)
} catch (error) {
console.error('Error updating order:', error)
return NextResponse.json(
{ error: 'Failed to update order' },
{ status: 500 }
)
}
}
// Helper function to encrypt password
function encryptPassword(password: string): string {
// TODO: Implement proper encryption using environment-based key
// For now, use a simple encryption for development
const key = crypto.scryptSync(
process.env.ENCRYPTION_KEY || 'dev-key-change-in-production',
'salt',
32
)
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
let encrypted = cipher.update(password, 'utf8', 'hex')
encrypted += cipher.final('hex')
return iv.toString('hex') + ':' + encrypted
}

View File

@@ -0,0 +1,144 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { OrderStatus, SubscriptionTier, Prisma } from '@prisma/client'
/**
* GET /api/v1/admin/orders
* List all orders with optional filters
*/
export async function GET(request: NextRequest) {
try {
const session = await auth()
// Check authentication and authorization
if (!session || session.user.userType !== 'staff') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Parse query parameters
const searchParams = request.nextUrl.searchParams
const status = searchParams.get('status') as OrderStatus | null
const tier = searchParams.get('tier')
const search = searchParams.get('search')
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '50')
// Build where clause
const where: Prisma.OrderWhereInput = {}
if (status) {
where.status = status
}
if (tier && Object.values(SubscriptionTier).includes(tier as SubscriptionTier)) {
where.tier = tier as SubscriptionTier
}
if (search) {
where.OR = [
{ domain: { contains: search, mode: 'insensitive' } },
{ user: { email: { contains: search, mode: 'insensitive' } } },
]
}
// Get orders with pagination
const [orders, total] = await Promise.all([
prisma.order.findMany({
where,
include: {
user: {
select: {
id: true,
name: true,
email: true,
company: true,
},
},
_count: {
select: { provisioningLogs: true },
},
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit,
}),
prisma.order.count({ where }),
])
return NextResponse.json({
orders,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
})
} catch (error) {
console.error('Error listing orders:', error)
return NextResponse.json(
{ error: 'Failed to list orders' },
{ status: 500 }
)
}
}
/**
* POST /api/v1/admin/orders
* Create a new order (admin-initiated)
*/
export async function POST(request: NextRequest) {
try {
const session = await auth()
if (!session || session.user.userType !== 'staff') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { userId, domain, tier, tools } = body
if (!userId || !domain || !tier || !tools) {
return NextResponse.json(
{ error: 'userId, domain, tier, and tools are required' },
{ status: 400 }
)
}
// Verify user exists
const user = await prisma.user.findUnique({ where: { id: userId } })
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
// Create order
const order = await prisma.order.create({
data: {
userId,
domain,
tier,
tools,
status: OrderStatus.PAYMENT_CONFIRMED,
configJson: { tools, tier, domain },
},
include: {
user: {
select: {
id: true,
name: true,
email: true,
},
},
},
})
return NextResponse.json(order, { status: 201 })
} catch (error) {
console.error('Error creating order:', error)
return NextResponse.json(
{ error: 'Failed to create order' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,126 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { OrderStatus, Prisma } from '@prisma/client'
/**
* GET /api/v1/admin/servers
* List all servers (orders with server credentials)
*/
export async function GET(request: NextRequest) {
try {
const session = await auth()
if (!session || session.user.userType !== 'staff') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const searchParams = request.nextUrl.searchParams
const status = searchParams.get('status')
const search = searchParams.get('search')
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '50')
// Build where clause - only orders with serverIp are considered servers
const where: Prisma.OrderWhereInput = {
serverIp: { not: null },
}
// Filter by server status (derived from order status)
if (status === 'online') {
where.status = { in: [OrderStatus.FULFILLED, OrderStatus.EMAIL_CONFIGURED] }
} else if (status === 'provisioning') {
where.status = { in: [OrderStatus.PROVISIONING, OrderStatus.DNS_READY, OrderStatus.SERVER_READY] }
} else if (status === 'offline') {
where.status = OrderStatus.FAILED
}
// Search by domain or serverIp
if (search) {
where.OR = [
{ domain: { contains: search, mode: 'insensitive' } },
{ serverIp: { contains: search, mode: 'insensitive' } },
{ user: { name: { contains: search, mode: 'insensitive' } } },
{ user: { company: { contains: search, mode: 'insensitive' } } },
]
}
const [orders, total] = await Promise.all([
prisma.order.findMany({
where,
select: {
id: true,
domain: true,
tier: true,
status: true,
serverIp: true,
sshPort: true,
tools: true,
createdAt: true,
serverReadyAt: true,
completedAt: true,
user: {
select: {
id: true,
name: true,
email: true,
company: true,
},
},
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit,
}),
prisma.order.count({ where }),
])
// Map orders to server format
const servers = orders.map((order) => ({
id: order.id,
domain: order.domain,
tier: order.tier,
orderStatus: order.status,
serverStatus: deriveServerStatus(order.status),
serverIp: order.serverIp,
sshPort: order.sshPort,
tools: order.tools,
createdAt: order.createdAt,
serverReadyAt: order.serverReadyAt,
completedAt: order.completedAt,
customer: order.user,
}))
return NextResponse.json({
servers,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
})
} catch (error) {
console.error('Error listing servers:', error)
return NextResponse.json(
{ error: 'Failed to list servers' },
{ status: 500 }
)
}
}
function deriveServerStatus(orderStatus: OrderStatus): 'online' | 'provisioning' | 'offline' | 'pending' {
switch (orderStatus) {
case OrderStatus.FULFILLED:
case OrderStatus.EMAIL_CONFIGURED:
return 'online'
case OrderStatus.PROVISIONING:
case OrderStatus.DNS_READY:
case OrderStatus.SERVER_READY:
return 'provisioning'
case OrderStatus.FAILED:
return 'offline'
default:
return 'pending'
}
}

View File

@@ -0,0 +1,170 @@
import { NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { OrderStatus, SubscriptionPlan, SubscriptionTier, UserStatus, SubscriptionStatus } from '@prisma/client'
/**
* GET /api/v1/admin/stats
* Get dashboard statistics
*/
export async function GET() {
try {
const session = await auth()
if (!session || session.user.userType !== 'staff') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Get order counts by status
const ordersByStatus = await prisma.order.groupBy({
by: ['status'],
_count: { status: true },
})
const orderStatusCounts: Record<OrderStatus, number> = Object.fromEntries(
Object.values(OrderStatus).map((status) => [status, 0])
) as Record<OrderStatus, number>
ordersByStatus.forEach((item) => {
orderStatusCounts[item.status] = item._count.status
})
// Calculate order statistics
const pendingStatuses = [
OrderStatus.PAYMENT_CONFIRMED,
OrderStatus.AWAITING_SERVER,
OrderStatus.SERVER_READY,
OrderStatus.DNS_PENDING,
OrderStatus.DNS_READY,
]
const inProgressStatuses = [OrderStatus.PROVISIONING]
const completedStatuses = [OrderStatus.FULFILLED, OrderStatus.EMAIL_CONFIGURED]
const failedStatuses = [OrderStatus.FAILED]
const ordersPending = pendingStatuses.reduce(
(sum, status) => sum + orderStatusCounts[status],
0
)
const ordersInProgress = inProgressStatuses.reduce(
(sum, status) => sum + orderStatusCounts[status],
0
)
const ordersCompleted = completedStatuses.reduce(
(sum, status) => sum + orderStatusCounts[status],
0
)
const ordersFailed = failedStatuses.reduce(
(sum, status) => sum + orderStatusCounts[status],
0
)
// Get customer counts by status
const customersByStatus = await prisma.user.groupBy({
by: ['status'],
_count: { status: true },
})
const customerStatusCounts: Record<UserStatus, number> = Object.fromEntries(
Object.values(UserStatus).map((status) => [status, 0])
) as Record<UserStatus, number>
customersByStatus.forEach((item) => {
customerStatusCounts[item.status] = item._count.status
})
// Get subscription counts
const subscriptionsByPlan = await prisma.subscription.groupBy({
by: ['plan'],
_count: { plan: true },
})
const subscriptionsByTier = await prisma.subscription.groupBy({
by: ['tier'],
_count: { tier: true },
})
const subscriptionsByStatusRaw = await prisma.subscription.groupBy({
by: ['status'],
_count: { status: true },
})
const planCounts: Record<SubscriptionPlan, number> = Object.fromEntries(
Object.values(SubscriptionPlan).map((plan) => [plan, 0])
) as Record<SubscriptionPlan, number>
subscriptionsByPlan.forEach((item) => {
planCounts[item.plan] = item._count.plan
})
const tierCounts: Record<SubscriptionTier, number> = Object.fromEntries(
Object.values(SubscriptionTier).map((tier) => [tier, 0])
) as Record<SubscriptionTier, number>
subscriptionsByTier.forEach((item) => {
tierCounts[item.tier] = item._count.tier
})
const subscriptionStatusCounts: Record<SubscriptionStatus, number> = Object.fromEntries(
Object.values(SubscriptionStatus).map((status) => [status, 0])
) as Record<SubscriptionStatus, number>
subscriptionsByStatusRaw.forEach((item) => {
subscriptionStatusCounts[item.status] = item._count.status
})
// Get recent orders
const recentOrders = await prisma.order.findMany({
take: 5,
orderBy: { createdAt: 'desc' },
include: {
user: {
select: {
id: true,
name: true,
email: true,
company: true,
},
},
_count: {
select: { provisioningLogs: true },
},
},
})
// Calculate totals
const ordersTotal = Object.values(orderStatusCounts).reduce((a, b) => a + b, 0)
const customersTotal = Object.values(customerStatusCounts).reduce((a, b) => a + b, 0)
const subscriptionsTotal = Object.values(planCounts).reduce((a, b) => a + b, 0)
return NextResponse.json({
orders: {
total: ordersTotal,
pending: ordersPending,
inProgress: ordersInProgress,
completed: ordersCompleted,
failed: ordersFailed,
byStatus: orderStatusCounts,
},
customers: {
total: customersTotal,
active: customerStatusCounts[UserStatus.ACTIVE],
suspended: customerStatusCounts[UserStatus.SUSPENDED],
pending: customerStatusCounts[UserStatus.PENDING_VERIFICATION],
},
subscriptions: {
total: subscriptionsTotal,
trial: subscriptionStatusCounts[SubscriptionStatus.TRIAL],
active: subscriptionStatusCounts[SubscriptionStatus.ACTIVE],
byPlan: planCounts,
byTier: tierCounts,
},
recentOrders,
})
} catch (error) {
console.error('Error getting dashboard stats:', error)
return NextResponse.json(
{ error: 'Failed to get dashboard stats' },
{ status: 500 }
)
}
}