Skip to content

admin-dashboard

<!-- Loaded by nextjs-ecommerce-engineer when task involves admin UI, order management, product CRUD, inventory tracking, analytics, or role-based access -->

From plugin
vexjoy-agent
413198 skills198 agents10 commands86 hooks
Install
$ npx -y skills add notque/vexjoy-agent --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

<!-- Loaded by nextjs-ecommerce-engineer when task involves admin UI, order management, product CRUD, inventory tracking, analytics, or role-based access -->

Agent definition

admin-dashboard.md

Admin Dashboard Reference

<!-- Loaded by nextjs-ecommerce-engineer when task involves admin UI, order management, product CRUD, inventory tracking, analytics, or role-based access -->

Every destructive action needs confirmation; every mutation needs authorization.

Role-Based Access Control

**When to use:** Any admin route. Gate at middleware and repeat in Server Components (defense in depth).

// lib/auth.ts — extend NextAuth session with role
import { DefaultSession } from 'next-auth'

declare module 'next-auth' {
  interface Session {
    user: DefaultSession['user'] & {
      id: string
      role: 'customer' | 'admin' | 'staff'
    }
  }
}

export const authOptions: NextAuthOptions = {
  callbacks: {
    session({ session, token }) {
      return {
        ...session,
        user: {
          ...session.user,
          id: token.sub!,
          role: token.role as 'customer' | 'admin' | 'staff',
        },
      }
    },
    jwt({ token, user }) {
      if (user) token.role = (user as any).role
      return token
    },
  },
}
// middleware.ts — block non-admin at the edge
export async function middleware(request: NextRequest) {
  if (!request.nextUrl.pathname.startsWith('/admin')) return NextResponse.next()

  const token = await getToken({ req: request })
  if (!token || token.role !== 'admin') {
    return NextResponse.redirect(new URL('/', request.url))
  }

  return NextResponse.next()
}

export const config = { matcher: ['/admin/:path*'] }
// app/admin/layout.tsx — secondary check in Server Component
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
  const session = await getServerSession(authOptions)

  // Belt and suspenders — middleware already blocked, but verify again
  if (session?.user?.role !== 'admin') notFound()

  return <div className="admin-shell">{children}</div>
}

---

Order Management

**When to use:** Admin view of all orders with filtering, status updates, and fulfillment tracking.

// app/admin/orders/page.tsx
import { db } from '@/lib/db'

interface OrdersPageProps {
  searchParams: { status?: string; page?: string }
}

export default async function OrdersPage({ searchParams }: OrdersPageProps) {
  const page = parseInt(searchParams.page ?? '1')
  const pageSize = 25
  const status = searchParams.status

  const [orders, total] = await Promise.all([
    db.order.findMany({
      where: status ? { status: status as OrderStatus } : undefined,
      include: {
        user: { select: { name: true, email: true } },
        items: {
          include: { product: { select: { name: true } } },
        },
      },
      orderBy: { createdAt: 'desc' },
      skip: (page - 1) * pageSize,
      take: pageSize,
    }),
    db.order.count({ where: status ? { status: status as OrderStatus } : undefined }),
  ])

  return (
    <div>
      <OrderFilters currentStatus={status} />
      <OrderTable orders={orders} />
      <Pagination page={page} total={total} pageSize={pageSize} />
    </div>
  )
}
// actions/admin/orders.ts — update order status
'use server'
import { z } from 'zod'
import { requireAdmin } from '@/lib/auth-helpers'

const UpdateOrderSchema = z.object({
  orderId: z.string().cuid(),
  status: z.enum(['pending', 'processing', 'shipped', 'delivered', 'refunded', 'cancelled']),
  note: z.string().max(500).optional(),
})

export async function updateOrderStatus(formData: FormData) {
  await requireAdmin() // throws if not admin

  const parsed = UpdateOrderSchema.safeParse({
    orderId: formData.get('orderId'),
    status: formData.get('status'),
    note: formData.get('note'),
  })

  if (!parsed.success) throw new Error('Invalid form data')

  await db.$transaction(async (tx) => {
    await tx.order.update({
      where: { id: parsed.data.orderId },
      data: { status: parsed.data.status },
    })
    // Audit log — every status change is traceable
    await tx.orderAuditLog.create({
      data: {
        orderId: parsed.data.orderId,
        previousStatus: 'unknown', // fetch before update in real usage
        newStatus: parsed.data.status,
        note: parsed.data.note,
        adminId: (await getServerSession(authOptions))!.user.id,
      },
    })
  })

  revalidatePath('/admin/orders')
}

---

Product CRUD

**When to use:** Admin product management — create, edit, archive products. Use `archive` instead of `delete` to preserve order history references.

// actions/admin/products.ts
'use server'
import { z } from 'zod'
import { requireAdmin } from '@/lib/auth-helpers'

const ProductSchema = z.object({
  name:        z.string().min(1).max(200),
  description: z.string().max(5000),
  price:       z.number().positive().multipleOf(0.01),
  stock:       z.number().int().min(0),
  categoryId:  z.string().cuid(),
  published:   z.boolean().default(false),
})

export async function createProduct(formData: FormData) {
  await requireAdmin()

  const parsed = ProductSchema.safeParse({
    name:        formData.get('name'),
    description: formData.get('description'),
    price:       parseFloat(formData.get('price') as string),
    stock:       parseInt(formData.get('stock') as string),
    categoryId:  formData.get('categoryId'),
    published:   formData.get('published') === 'true',
  })

  if (!parsed.success) {
    return { success: false, errors: parsed.error.flatten().fieldErrors }
  }

  const product = await db.product.create({ data: parsed.data })

  revalidatePath('/admin/products')
  revalidatePath('/products') // invalidate public product listing

  return { success: true, productId: product.id }
}

export async function archiveProduct(productId: string) {
  await requireAdmin()

  // Soft delete — preserve for order history
  await db.product.update({
    where: { id: productId },
    data: { archivedAt: new Date(), published: false },
  })

  revalidatePath('/admin/products')
  revalidate
Read more
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.

Get the whole plugin, auto-invoked