first commit
This commit is contained in:
69
app/api/achievements/route.ts
Normal file
69
app/api/achievements/route.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { rankSystem } from '@/lib/ranks'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
|
||||
// Get user with current rank
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
currentRank: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Calculate current metrics
|
||||
const metrics = await rankSystem.calculateUserMetrics(userId)
|
||||
|
||||
// Get all ranks to find next rank
|
||||
const allRanks = await prisma.rank.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: { order: 'asc' }
|
||||
})
|
||||
|
||||
// Find next rank
|
||||
const currentRankOrder = user.currentRank?.order || 0
|
||||
const nextRank = allRanks.find(rank => rank.order > currentRankOrder)
|
||||
|
||||
// Get user's achievements
|
||||
const achievements = await prisma.rankAchievement.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
rank: true
|
||||
},
|
||||
orderBy: { achievedAt: 'desc' }
|
||||
})
|
||||
|
||||
const response = {
|
||||
currentRank: user.currentRank,
|
||||
nextRank: nextRank || null,
|
||||
metrics,
|
||||
achievements: achievements.map(achievement => ({
|
||||
id: achievement.id,
|
||||
rank: achievement.rank,
|
||||
achievedAt: achievement.achievedAt.toISOString()
|
||||
}))
|
||||
}
|
||||
|
||||
return NextResponse.json(response)
|
||||
} catch (error) {
|
||||
console.error('Error fetching achievements:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch achievements' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
195
app/api/admin/categories/[id]/route.ts
Normal file
195
app/api/admin/categories/[id]/route.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
// Check if category exists
|
||||
const existingCategory = await prisma.category.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
products: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!existingCategory) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Category not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if category has products
|
||||
if (existingCategory._count.products > 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Cannot delete category with associated products',
|
||||
productCount: existingCategory._count.products
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Delete the category
|
||||
await prisma.category.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Category deleted successfully',
|
||||
deletedCategory: {
|
||||
id: existingCategory.id,
|
||||
name: existingCategory.name
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('Error deleting category:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete category' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const { name, description, image, isActive } = body
|
||||
|
||||
// Check if category exists
|
||||
const existingCategory = await prisma.category.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
if (!existingCategory) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Category not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// If name is being updated, check for uniqueness
|
||||
if (name && name !== existingCategory.name) {
|
||||
const nameExists = await prisma.category.findUnique({
|
||||
where: { name }
|
||||
})
|
||||
|
||||
if (nameExists) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Category name already exists' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the category
|
||||
const updatedCategory = await prisma.category.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(name !== undefined && { name }),
|
||||
...(description !== undefined && { description }),
|
||||
...(image !== undefined && { image }),
|
||||
...(isActive !== undefined && { isActive })
|
||||
},
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
products: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json(updatedCategory)
|
||||
} catch (error: any) {
|
||||
console.error('Error updating category:', error)
|
||||
|
||||
// Handle Prisma unique constraint errors
|
||||
if (error.code === 'P2002') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Category name already exists' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update category' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
const category = await prisma.category.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
products: true
|
||||
}
|
||||
},
|
||||
products: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
price: true,
|
||||
isActive: true
|
||||
},
|
||||
take: 10 // Limit to first 10 products for preview
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!category) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Category not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(category)
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching category:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch category' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
73
app/api/admin/categories/route.ts
Normal file
73
app/api/admin/categories/route.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const search = searchParams.get('search')
|
||||
|
||||
const categories = await prisma.category.findMany({
|
||||
where: search ? {
|
||||
name: {
|
||||
contains: search,
|
||||
mode: 'insensitive'
|
||||
}
|
||||
} : {},
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
products: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json(categories)
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch categories' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { name, description, image, isActive } = body
|
||||
|
||||
const category = await prisma.category.create({
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
image,
|
||||
isActive
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json(category)
|
||||
} catch (error) {
|
||||
console.error('Error creating category:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create category' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
43
app/api/admin/commission-settings/route.ts
Normal file
43
app/api/admin/commission-settings/route.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const settings = await prisma.commissionSettings.findMany({
|
||||
orderBy: { level: 'asc' },
|
||||
})
|
||||
|
||||
return NextResponse.json(settings)
|
||||
} catch (error) {
|
||||
console.error('Commission settings API error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { level, percentage, isActive } = await request.json()
|
||||
|
||||
const setting = await prisma.commissionSettings.upsert({
|
||||
where: { level },
|
||||
update: { percentage, isActive },
|
||||
create: { level, percentage, isActive },
|
||||
})
|
||||
|
||||
return NextResponse.json(setting)
|
||||
} catch (error) {
|
||||
console.error('Create commission setting error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
37
app/api/admin/commissions/[id]/route.ts
Normal file
37
app/api/admin/commissions/[id]/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: commissionId } = await params
|
||||
const { status } = await request.json()
|
||||
|
||||
// Validate status
|
||||
if (!['PENDING', 'APPROVED', 'PAID', 'CANCELLED'].includes(status)) {
|
||||
return NextResponse.json({ error: 'Invalid status' }, { status: 400 })
|
||||
}
|
||||
|
||||
const commission = await prisma.commission.update({
|
||||
where: { id: commissionId },
|
||||
data: { status }
|
||||
})
|
||||
|
||||
return NextResponse.json(commission)
|
||||
} catch (error) {
|
||||
console.error('Error updating commission:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update commission' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
77
app/api/admin/commissions/route.ts
Normal file
77
app/api/admin/commissions/route.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const url = new URL(request.url)
|
||||
const status = url.searchParams.get('status')
|
||||
const type = url.searchParams.get('type')
|
||||
const level = url.searchParams.get('level')
|
||||
const page = parseInt(url.searchParams.get('page') || '1')
|
||||
const limit = parseInt(url.searchParams.get('limit') || '50')
|
||||
|
||||
const where: any = {}
|
||||
|
||||
if (status && status !== 'all') {
|
||||
where.status = status
|
||||
}
|
||||
|
||||
if (type && type !== 'all') {
|
||||
where.type = type
|
||||
}
|
||||
|
||||
if (level && level !== 'all') {
|
||||
where.level = parseInt(level)
|
||||
}
|
||||
|
||||
const commissions = await prisma.commission.findMany({
|
||||
where,
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true
|
||||
}
|
||||
},
|
||||
fromUser: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
skip: (page - 1) * limit,
|
||||
take: limit
|
||||
})
|
||||
|
||||
const total = await prisma.commission.count({ where })
|
||||
|
||||
return NextResponse.json({
|
||||
commissions: commissions || [],
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching admin commissions:', error)
|
||||
return NextResponse.json({
|
||||
commissions: [],
|
||||
pagination: { page: 1, limit: 50, total: 0, pages: 0 }
|
||||
})
|
||||
}
|
||||
}
|
||||
36
app/api/admin/commissions/settings/[id]/route.ts
Normal file
36
app/api/admin/commissions/settings/[id]/route.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: settingId } = await params
|
||||
const { level, percentage, isActive } = await request.json()
|
||||
|
||||
const setting = await prisma.commissionSettings.update({
|
||||
where: { id: settingId },
|
||||
data: {
|
||||
level,
|
||||
percentage,
|
||||
isActive
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json(setting)
|
||||
} catch (error) {
|
||||
console.error('Error updating commission setting:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update commission setting' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
41
app/api/admin/commissions/settings/route.ts
Normal file
41
app/api/admin/commissions/settings/route.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
let settings = await prisma.commissionSettings.findMany({
|
||||
orderBy: { level: 'asc' }
|
||||
})
|
||||
|
||||
// If no settings exist, create default ones
|
||||
if (settings.length === 0) {
|
||||
const defaultSettings = [
|
||||
{ level: 1, percentage: 10, isActive: true },
|
||||
{ level: 2, percentage: 5, isActive: true },
|
||||
{ level: 3, percentage: 3, isActive: true },
|
||||
{ level: 4, percentage: 2, isActive: true },
|
||||
{ level: 5, percentage: 1, isActive: true }
|
||||
]
|
||||
|
||||
await prisma.commissionSettings.createMany({
|
||||
data: defaultSettings
|
||||
})
|
||||
|
||||
settings = await prisma.commissionSettings.findMany({
|
||||
orderBy: { level: 'asc' }
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ settings: settings || [] })
|
||||
} catch (error) {
|
||||
console.error('Error fetching commission settings:', error)
|
||||
return NextResponse.json({ settings: [] })
|
||||
}
|
||||
}
|
||||
66
app/api/admin/commissions/stats/route.ts
Normal file
66
app/api/admin/commissions/stats/route.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get total commissions count
|
||||
const totalCommissions = await prisma.commission.count()
|
||||
|
||||
// Get amounts by status
|
||||
const [pendingResult, approvedResult, paidResult] = await Promise.all([
|
||||
prisma.commission.aggregate({
|
||||
where: { status: 'PENDING' },
|
||||
_sum: { amount: true }
|
||||
}),
|
||||
prisma.commission.aggregate({
|
||||
where: { status: 'APPROVED' },
|
||||
_sum: { amount: true }
|
||||
}),
|
||||
prisma.commission.aggregate({
|
||||
where: { status: 'PAID' },
|
||||
_sum: { amount: true }
|
||||
})
|
||||
])
|
||||
|
||||
// Get this month commissions
|
||||
const startOfMonth = new Date()
|
||||
startOfMonth.setDate(1)
|
||||
startOfMonth.setHours(0, 0, 0, 0)
|
||||
|
||||
const thisMonthCommissions = await prisma.commission.count({
|
||||
where: {
|
||||
createdAt: { gte: startOfMonth }
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate average commission
|
||||
const totalAmount = (pendingResult._sum.amount || 0) + (approvedResult._sum.amount || 0) + (paidResult._sum.amount || 0)
|
||||
const averageCommission = totalCommissions > 0 ? totalAmount / totalCommissions : 0
|
||||
|
||||
return NextResponse.json({
|
||||
totalCommissions: totalCommissions || 0,
|
||||
pendingAmount: pendingResult._sum.amount || 0,
|
||||
approvedAmount: approvedResult._sum.amount || 0,
|
||||
paidAmount: paidResult._sum.amount || 0,
|
||||
thisMonthCommissions: thisMonthCommissions || 0,
|
||||
averageCommission: averageCommission || 0
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching commission stats:', error)
|
||||
return NextResponse.json({
|
||||
totalCommissions: 0,
|
||||
pendingAmount: 0,
|
||||
approvedAmount: 0,
|
||||
paidAmount: 0,
|
||||
thisMonthCommissions: 0,
|
||||
averageCommission: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
34
app/api/admin/dashboard/recent-orders/route.ts
Normal file
34
app/api/admin/dashboard/recent-orders/route.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const orders = await prisma.order.findMany({
|
||||
take: 10,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({ orders })
|
||||
} catch (error) {
|
||||
console.error('Error fetching recent orders:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch recent orders' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
128
app/api/admin/dashboard/stats/route.ts
Normal file
128
app/api/admin/dashboard/stats/route.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get current date ranges
|
||||
const now = new Date()
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||
const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1)
|
||||
const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0)
|
||||
|
||||
// Get current totals
|
||||
const [totalUsers, totalProducts, totalOrders, totalRevenueResult] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
prisma.product.count({ where: { isActive: true } }),
|
||||
prisma.order.count(),
|
||||
prisma.order.aggregate({
|
||||
where: { status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] } },
|
||||
_sum: { total: true }
|
||||
})
|
||||
])
|
||||
|
||||
// Get last month data for comparison
|
||||
const [lastMonthUsers, lastMonthProducts, lastMonthOrders, lastMonthRevenueResult] = await Promise.all([
|
||||
prisma.user.count({
|
||||
where: {
|
||||
joinedAt: {
|
||||
gte: startOfLastMonth,
|
||||
lte: endOfLastMonth
|
||||
}
|
||||
}
|
||||
}),
|
||||
prisma.product.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
createdAt: {
|
||||
gte: startOfLastMonth,
|
||||
lte: endOfLastMonth
|
||||
}
|
||||
}
|
||||
}),
|
||||
prisma.order.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startOfLastMonth,
|
||||
lte: endOfLastMonth
|
||||
}
|
||||
}
|
||||
}),
|
||||
prisma.order.aggregate({
|
||||
where: {
|
||||
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] },
|
||||
createdAt: {
|
||||
gte: startOfLastMonth,
|
||||
lte: endOfLastMonth
|
||||
}
|
||||
},
|
||||
_sum: { total: true }
|
||||
})
|
||||
])
|
||||
|
||||
// Get this month data
|
||||
const [thisMonthUsers, thisMonthProducts, thisMonthOrders, thisMonthRevenueResult] = await Promise.all([
|
||||
prisma.user.count({
|
||||
where: {
|
||||
joinedAt: { gte: startOfMonth }
|
||||
}
|
||||
}),
|
||||
prisma.product.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
createdAt: { gte: startOfMonth }
|
||||
}
|
||||
}),
|
||||
prisma.order.count({
|
||||
where: {
|
||||
createdAt: { gte: startOfMonth }
|
||||
}
|
||||
}),
|
||||
prisma.order.aggregate({
|
||||
where: {
|
||||
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] },
|
||||
createdAt: { gte: startOfMonth }
|
||||
},
|
||||
_sum: { total: true }
|
||||
})
|
||||
])
|
||||
|
||||
// Calculate growth percentages
|
||||
const calculateGrowth = (current: number, previous: number) => {
|
||||
if (previous === 0) return current > 0 ? 100 : 0
|
||||
return ((current - previous) / previous) * 100
|
||||
}
|
||||
|
||||
const totalRevenue = totalRevenueResult._sum.total || 0
|
||||
const lastMonthRevenue = lastMonthRevenueResult._sum.total || 0
|
||||
const thisMonthRevenue = thisMonthRevenueResult._sum.total || 0
|
||||
|
||||
const userGrowth = calculateGrowth(thisMonthUsers, lastMonthUsers)
|
||||
const productGrowth = calculateGrowth(thisMonthProducts, lastMonthProducts)
|
||||
const orderGrowth = calculateGrowth(thisMonthOrders, lastMonthOrders)
|
||||
const revenueGrowth = calculateGrowth(thisMonthRevenue, lastMonthRevenue)
|
||||
|
||||
return NextResponse.json({
|
||||
totalUsers,
|
||||
totalProducts,
|
||||
totalOrders,
|
||||
totalRevenue,
|
||||
userGrowth,
|
||||
productGrowth,
|
||||
orderGrowth,
|
||||
revenueGrowth
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard stats:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch dashboard stats' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
59
app/api/admin/dashboard/top-products/route.ts
Normal file
59
app/api/admin/dashboard/top-products/route.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get top products by order count this month
|
||||
const startOfMonth = new Date()
|
||||
startOfMonth.setDate(1)
|
||||
startOfMonth.setHours(0, 0, 0, 0)
|
||||
|
||||
const topProducts = await prisma.product.findMany({
|
||||
take: 5,
|
||||
include: {
|
||||
orderItems: {
|
||||
where: {
|
||||
order: {
|
||||
createdAt: { gte: startOfMonth },
|
||||
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] }
|
||||
}
|
||||
},
|
||||
select: {
|
||||
quantity: true,
|
||||
price: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const productsWithStats = topProducts.map(product => {
|
||||
const orderCount = product.orderItems.reduce((sum, item) => sum + item.quantity, 0)
|
||||
const totalRevenue = product.orderItems.reduce((sum, item) => sum + (item.price * item.quantity), 0)
|
||||
|
||||
return {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
orderCount,
|
||||
totalRevenue
|
||||
}
|
||||
}).filter(product => product.orderCount > 0)
|
||||
.sort((a, b) => b.orderCount - a.orderCount)
|
||||
.slice(0, 5)
|
||||
|
||||
return NextResponse.json({ products: productsWithStats })
|
||||
} catch (error) {
|
||||
console.error('Error fetching top products:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch top products' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
148
app/api/admin/orders/[id]/route.ts
Normal file
148
app/api/admin/orders/[id]/route.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
phone: true
|
||||
}
|
||||
},
|
||||
orderItems: {
|
||||
include: {
|
||||
product: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
images: true,
|
||||
sku: true,
|
||||
price: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
shippingAddress: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
company: true,
|
||||
address1: true,
|
||||
address2: true,
|
||||
city: true,
|
||||
state: true,
|
||||
zipCode: true,
|
||||
country: true,
|
||||
phone: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
return NextResponse.json({ error: 'Order not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json(order)
|
||||
} catch (error) {
|
||||
console.error('Error fetching order:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch order' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const { status } = body
|
||||
|
||||
if (!['PENDING', 'PAID', 'SHIPPED', 'DELIVERED', 'CANCELLED'].includes(status)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid status' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const order = await prisma.order.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status,
|
||||
updatedAt: new Date()
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
phone: true
|
||||
}
|
||||
},
|
||||
orderItems: {
|
||||
include: {
|
||||
product: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
images: true,
|
||||
sku: true,
|
||||
price: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
shippingAddress: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
company: true,
|
||||
address1: true,
|
||||
address2: true,
|
||||
city: true,
|
||||
state: true,
|
||||
zipCode: true,
|
||||
country: true,
|
||||
phone: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json(order)
|
||||
} catch (error) {
|
||||
console.error('Error updating order:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update order' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
48
app/api/admin/orders/bulk/route.ts
Normal file
48
app/api/admin/orders/bulk/route.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { orderIds, status } = body
|
||||
|
||||
if (!orderIds || !Array.isArray(orderIds) || orderIds.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Order IDs are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!['PENDING', 'PAID', 'SHIPPED', 'DELIVERED', 'CANCELLED'].includes(status)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid status' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
await prisma.order.updateMany({
|
||||
where: {
|
||||
id: { in: orderIds }
|
||||
},
|
||||
data: {
|
||||
status,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Error in bulk order update:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to perform bulk update' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
72
app/api/admin/orders/route.ts
Normal file
72
app/api/admin/orders/route.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const search = searchParams.get('search')
|
||||
const status = searchParams.get('status')
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '20')
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
const where: any = {}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ id: { contains: search, mode: 'insensitive' } },
|
||||
{ razorpayOrderId: { contains: search, mode: 'insensitive' } },
|
||||
{ user: { name: { contains: search, mode: 'insensitive' } } },
|
||||
{ user: { email: { contains: search, mode: 'insensitive' } } }
|
||||
]
|
||||
}
|
||||
|
||||
if (status) {
|
||||
where.status = status
|
||||
}
|
||||
|
||||
const orders = await prisma.order.findMany({
|
||||
where,
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true
|
||||
}
|
||||
},
|
||||
orderItems: {
|
||||
include: {
|
||||
product: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
images: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
skip,
|
||||
take: limit
|
||||
})
|
||||
|
||||
return NextResponse.json(orders)
|
||||
} catch (error) {
|
||||
console.error('Error fetching orders:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch orders' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
66
app/api/admin/payouts/[id]/route.ts
Normal file
66
app/api/admin/payouts/[id]/route.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: payoutId } = await params
|
||||
const { status, adminNotes } = await request.json()
|
||||
|
||||
// Validate status
|
||||
if (!['APPROVED', 'REJECTED', 'PAID'].includes(status)) {
|
||||
return NextResponse.json({ error: 'Invalid status' }, { status: 400 })
|
||||
}
|
||||
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
// Get the payout
|
||||
const payout = await tx.payout.findUnique({
|
||||
where: { id: payoutId },
|
||||
include: { user: true }
|
||||
})
|
||||
|
||||
if (!payout) {
|
||||
throw new Error('Payout not found')
|
||||
}
|
||||
|
||||
// Update payout
|
||||
const updatedPayout = await tx.payout.update({
|
||||
where: { id: payoutId },
|
||||
data: {
|
||||
status,
|
||||
adminNotes,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
// If rejecting, restore the amount to user's wallet
|
||||
if (status === 'REJECTED') {
|
||||
await tx.wallet.update({
|
||||
where: { userId: payout.userId },
|
||||
data: {
|
||||
balance: { increment: payout.amount }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return updatedPayout
|
||||
})
|
||||
|
||||
return NextResponse.json(result)
|
||||
} catch (error) {
|
||||
console.error('Error updating payout:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to update payout' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
59
app/api/admin/payouts/route.ts
Normal file
59
app/api/admin/payouts/route.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const url = new URL(request.url)
|
||||
const status = url.searchParams.get('status')
|
||||
const page = parseInt(url.searchParams.get('page') || '1')
|
||||
const limit = parseInt(url.searchParams.get('limit') || '50')
|
||||
|
||||
const where: any = {}
|
||||
if (status && status !== 'all') {
|
||||
where.status = status
|
||||
}
|
||||
|
||||
const payouts = await prisma.payout.findMany({
|
||||
where,
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
skip: (page - 1) * limit,
|
||||
take: limit
|
||||
})
|
||||
|
||||
const total = await prisma.payout.count({ where })
|
||||
|
||||
return NextResponse.json({
|
||||
payouts,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching admin payouts:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch payouts' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
45
app/api/admin/payouts/stats/route.ts
Normal file
45
app/api/admin/payouts/stats/route.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get total requests
|
||||
const totalRequests = await prisma.payout.count()
|
||||
|
||||
// Get amounts by status
|
||||
const [pendingResult, approvedResult, paidResult] = await Promise.all([
|
||||
prisma.payout.aggregate({
|
||||
where: { status: 'PENDING' },
|
||||
_sum: { amount: true }
|
||||
}),
|
||||
prisma.payout.aggregate({
|
||||
where: { status: 'APPROVED' },
|
||||
_sum: { amount: true }
|
||||
}),
|
||||
prisma.payout.aggregate({
|
||||
where: { status: 'PAID' },
|
||||
_sum: { amount: true }
|
||||
})
|
||||
])
|
||||
|
||||
return NextResponse.json({
|
||||
totalRequests,
|
||||
pendingAmount: pendingResult._sum.amount || 0,
|
||||
approvedAmount: approvedResult._sum.amount || 0,
|
||||
paidAmount: paidResult._sum.amount || 0
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching payout stats:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch payout stats' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
114
app/api/admin/products/[id]/route.ts
Normal file
114
app/api/admin/products/[id]/route.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
const product = await prisma.product.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
category: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!product) {
|
||||
return NextResponse.json({ error: 'Product not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json(product)
|
||||
} catch (error) {
|
||||
console.error('Error fetching product:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch product' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const { name, description, price, discount, images, stock, sku, isActive, categoryId, weight } = body
|
||||
|
||||
const product = await prisma.product.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
price,
|
||||
discount,
|
||||
images,
|
||||
stock,
|
||||
sku,
|
||||
isActive,
|
||||
categoryId,
|
||||
weight,
|
||||
slug: name.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-') + '-' + Date.now()
|
||||
},
|
||||
include: {
|
||||
category: true
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json(product)
|
||||
} catch (error) {
|
||||
console.error('Error updating product:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update product' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
await prisma.product.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Error deleting product:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete product' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
61
app/api/admin/products/bulk/route.ts
Normal file
61
app/api/admin/products/bulk/route.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { productIds, action } = body
|
||||
|
||||
if (!productIds || !Array.isArray(productIds) || productIds.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Product IDs are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
let updateData: any = {}
|
||||
|
||||
switch (action) {
|
||||
case 'activate':
|
||||
updateData = { isActive: true }
|
||||
break
|
||||
case 'deactivate':
|
||||
updateData = { isActive: false }
|
||||
break
|
||||
case 'delete':
|
||||
await prisma.product.deleteMany({
|
||||
where: {
|
||||
id: { in: productIds }
|
||||
}
|
||||
})
|
||||
return NextResponse.json({ success: true })
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid action' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
await prisma.product.updateMany({
|
||||
where: {
|
||||
id: { in: productIds }
|
||||
},
|
||||
data: updateData
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Error in bulk action:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to perform bulk action' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
195
app/api/admin/products/export/route.ts
Normal file
195
app/api/admin/products/export/route.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { auth } from '@/auth'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const selectedColumns = searchParams.get('columns')?.split(',') || []
|
||||
const categoryId = searchParams.get('categoryId') || ''
|
||||
const isActive = searchParams.get('isActive') || ''
|
||||
const limit = parseInt(searchParams.get('limit') || '10000')
|
||||
|
||||
// Build where clause based on filters
|
||||
const whereClause: any = {}
|
||||
|
||||
if (categoryId && categoryId !== '') {
|
||||
whereClause.categoryId = categoryId
|
||||
}
|
||||
|
||||
if (isActive && isActive !== '') {
|
||||
whereClause.isActive = isActive === 'true'
|
||||
}
|
||||
|
||||
// Fetch products with category information
|
||||
const products = await prisma.product.findMany({
|
||||
where: whereClause,
|
||||
include: {
|
||||
category: {
|
||||
select: { name: true }
|
||||
}
|
||||
},
|
||||
take: limit,
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
|
||||
// Transform data based on selected columns
|
||||
const transformedData = products.map(product => {
|
||||
const baseData = {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
description: product.description || '',
|
||||
price: product.price,
|
||||
discount: product.discount,
|
||||
stock: product.stock,
|
||||
sku: product.sku,
|
||||
slug: product.slug,
|
||||
isActive: product.isActive,
|
||||
categoryName: product.category?.name || '',
|
||||
categoryId: product.categoryId || '',
|
||||
brand: product.brand || '',
|
||||
origin: product.origin || '',
|
||||
weight: product.weight || '',
|
||||
images: product.images.join(', '),
|
||||
createdAt: product.createdAt.toISOString(),
|
||||
updatedAt: product.updatedAt.toISOString(),
|
||||
// Calculated fields
|
||||
finalPrice: product.price - (product.price * product.discount / 100),
|
||||
discountAmount: product.price * product.discount / 100,
|
||||
stockStatus: product.stock > 10 ? 'In Stock' : product.stock > 0 ? 'Low Stock' : 'Out of Stock',
|
||||
status: product.isActive ? 'Active' : 'Inactive'
|
||||
}
|
||||
|
||||
// Filter by selected columns if specified
|
||||
if (selectedColumns.length > 0) {
|
||||
const filteredData: any = {}
|
||||
selectedColumns.forEach(column => {
|
||||
if (column in baseData) {
|
||||
filteredData[column] = (baseData as any)[column]
|
||||
}
|
||||
})
|
||||
return filteredData
|
||||
}
|
||||
|
||||
return baseData
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: transformedData,
|
||||
count: transformedData.length
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Product export error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const { columns = [], filters = {} } = await request.json()
|
||||
const selectedColumns = columns || []
|
||||
const categoryId = filters.categoryId || ''
|
||||
const isActive = filters.isActive || ''
|
||||
const limit = parseInt(filters.limit || '10000')
|
||||
|
||||
// Build where clause based on filters
|
||||
const whereClause: any = {}
|
||||
|
||||
if (categoryId && categoryId !== '') {
|
||||
whereClause.categoryId = categoryId
|
||||
}
|
||||
|
||||
if (isActive && isActive !== '') {
|
||||
whereClause.isActive = isActive === 'true'
|
||||
}
|
||||
|
||||
// Fetch products with category information
|
||||
const products = await prisma.product.findMany({
|
||||
where: whereClause,
|
||||
include: {
|
||||
category: {
|
||||
select: { name: true }
|
||||
}
|
||||
},
|
||||
take: limit,
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
|
||||
// Transform data based on selected columns
|
||||
const transformedData = products.map(product => {
|
||||
const baseData = {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
description: product.description || '',
|
||||
price: product.price,
|
||||
discount: product.discount,
|
||||
stock: product.stock,
|
||||
sku: product.sku,
|
||||
slug: product.slug,
|
||||
isActive: product.isActive,
|
||||
categoryName: product.category?.name || '',
|
||||
categoryId: product.categoryId || '',
|
||||
brand: product.brand || '',
|
||||
origin: product.origin || '',
|
||||
weight: product.weight || '',
|
||||
images: product.images.join(', '),
|
||||
createdAt: product.createdAt.toISOString(),
|
||||
updatedAt: product.updatedAt.toISOString(),
|
||||
// Calculated fields
|
||||
finalPrice: product.price - (product.price * product.discount / 100),
|
||||
discountAmount: product.price * product.discount / 100,
|
||||
stockStatus: product.stock > 10 ? 'In Stock' : product.stock > 0 ? 'Low Stock' : 'Out of Stock',
|
||||
status: product.isActive ? 'Active' : 'Inactive'
|
||||
}
|
||||
|
||||
// Filter by selected columns if specified
|
||||
if (selectedColumns.length > 0) {
|
||||
const filteredData: any = {}
|
||||
selectedColumns.forEach((column: string) => {
|
||||
if (column in baseData) {
|
||||
filteredData[column] = (baseData as any)[column]
|
||||
}
|
||||
})
|
||||
return filteredData
|
||||
}
|
||||
|
||||
return baseData
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: transformedData,
|
||||
count: transformedData.length
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Product export error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
162
app/api/admin/products/import/route.ts
Normal file
162
app/api/admin/products/import/route.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { auth } from '@/auth'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const { data } = await request.json()
|
||||
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'No products provided' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
let successCount = 0
|
||||
let errorCount = 0
|
||||
const errors: string[] = []
|
||||
|
||||
// Process products in batches to avoid overwhelming the database
|
||||
const batchSize = 50
|
||||
const batches: any[][] = []
|
||||
for (let i = 0; i < data.length; i += batchSize) {
|
||||
batches.push(data.slice(i, i + batchSize))
|
||||
}
|
||||
|
||||
for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
|
||||
const batch = batches[batchIndex]
|
||||
try {
|
||||
const productsToCreate: any[] = []
|
||||
const productsToUpdate: any[] = []
|
||||
|
||||
for (let index = 0; index < batch.length; index++) {
|
||||
const productData = batch[index]
|
||||
try {
|
||||
// Validate required fields
|
||||
if (!productData.name || !productData.price) {
|
||||
errors.push(`Row ${batchIndex * batchSize + index + 1}: Missing required fields (name, price)`)
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if category exists
|
||||
let categoryId: string | undefined = undefined
|
||||
if (productData.categoryId) {
|
||||
const category = await prisma.category.findUnique({
|
||||
where: { id: productData.categoryId }
|
||||
})
|
||||
if (category) {
|
||||
categoryId = category.id
|
||||
} else {
|
||||
errors.push(`Row ${batchIndex * batchSize + index + 1}: Category with ID '${productData.categoryId}' not found`)
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Generate SKU if not provided
|
||||
const sku = productData.sku || `PROD-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
// Prepare product data
|
||||
const productPayload = {
|
||||
name: productData.name.trim(),
|
||||
description: productData.description || '',
|
||||
price: parseFloat(productData.price) || 0,
|
||||
discount: parseFloat(productData.discount) || 0,
|
||||
stock: parseInt(productData.stock) || 0,
|
||||
sku: sku,
|
||||
slug: productData.slug || productData.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
||||
isActive: productData.active === 'true' || productData.active === true || productData.active === '1' || true,
|
||||
...(categoryId && { categoryId }),
|
||||
brand: productData.brand || '',
|
||||
origin: productData.origin || '',
|
||||
weight: productData.weight || '',
|
||||
images: productData.images ? (typeof productData.images === 'string' ? productData.images.split(',').map((img: string) => img.trim()) : []) : []
|
||||
}
|
||||
|
||||
// Check if product exists (by SKU)
|
||||
const existingProduct = await prisma.product.findUnique({
|
||||
where: { sku: productPayload.sku }
|
||||
})
|
||||
|
||||
if (existingProduct) {
|
||||
// Update existing product - create update data without null categoryId
|
||||
const updatePayload = { ...productPayload }
|
||||
if (!categoryId) {
|
||||
delete updatePayload.categoryId
|
||||
}
|
||||
|
||||
productsToUpdate.push({
|
||||
where: { id: existingProduct.id },
|
||||
data: updatePayload
|
||||
})
|
||||
} else {
|
||||
// Create new product
|
||||
productsToCreate.push(productPayload)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
errors.push(`Row ${batchIndex * batchSize + index + 1}: ${error instanceof Error ? error.message : 'Processing error'}`)
|
||||
errorCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Create new products
|
||||
if (productsToCreate.length > 0) {
|
||||
try {
|
||||
await prisma.product.createMany({
|
||||
data: productsToCreate,
|
||||
skipDuplicates: true
|
||||
})
|
||||
successCount += productsToCreate.length
|
||||
} catch (error) {
|
||||
errors.push(`Batch ${batchIndex + 1}: Failed to create products - ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
errorCount += productsToCreate.length
|
||||
}
|
||||
}
|
||||
|
||||
// Update existing products
|
||||
for (const updateData of productsToUpdate) {
|
||||
try {
|
||||
await prisma.product.update(updateData)
|
||||
successCount++
|
||||
} catch (error) {
|
||||
errors.push(`Failed to update product: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
errorCount++
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
errors.push(`Batch ${batchIndex + 1}: ${error instanceof Error ? error.message : 'Batch processing error'}`)
|
||||
errorCount += batch.length
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: errorCount === 0,
|
||||
message: errorCount === 0 ?
|
||||
`Successfully imported ${successCount} products` :
|
||||
`Import completed with errors. ${successCount} successful, ${errorCount} failed.`,
|
||||
successCount,
|
||||
errorCount,
|
||||
errors: errors.slice(0, 50) // Limit to first 50 errors
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Product import error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
54
app/api/admin/products/route.ts
Normal file
54
app/api/admin/products/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { name, description, price, discount, images, stock, sku, isActive, categoryId } = body
|
||||
|
||||
// Check if SKU already exists
|
||||
const existingSku = await prisma.product.findUnique({
|
||||
where: { sku }
|
||||
})
|
||||
|
||||
if (existingSku) {
|
||||
return NextResponse.json(
|
||||
{ error: 'SKU already exists' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const product = await prisma.product.create({
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
price,
|
||||
discount,
|
||||
images,
|
||||
stock,
|
||||
sku,
|
||||
isActive,
|
||||
categoryId,
|
||||
slug: name.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-') + '-' + Date.now()
|
||||
},
|
||||
include: {
|
||||
category: true
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json(product)
|
||||
} catch (error) {
|
||||
console.error('Error creating product:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create product' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
35
app/api/admin/reviews/[id]/approve/route.ts
Normal file
35
app/api/admin/reviews/[id]/approve/route.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ id: string }>
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: Props
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
const review = await prisma.review.update({
|
||||
where: { id },
|
||||
data: { isApproved: true }
|
||||
})
|
||||
|
||||
return NextResponse.json({ review })
|
||||
} catch (error) {
|
||||
console.error('Error approving review:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to approve review' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
35
app/api/admin/reviews/[id]/reject/route.ts
Normal file
35
app/api/admin/reviews/[id]/reject/route.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ id: string }>
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: Props
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
const review = await prisma.review.update({
|
||||
where: { id },
|
||||
data: { isApproved: false }
|
||||
})
|
||||
|
||||
return NextResponse.json({ review })
|
||||
} catch (error) {
|
||||
console.error('Error rejecting review:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to reject review' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
44
app/api/admin/reviews/[id]/route.ts
Normal file
44
app/api/admin/reviews/[id]/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ id: string }>
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: Props
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
// Delete related records first
|
||||
await prisma.reviewHelpfulVote.deleteMany({
|
||||
where: { reviewId: id }
|
||||
})
|
||||
|
||||
await prisma.reviewReport.deleteMany({
|
||||
where: { reviewId: id }
|
||||
})
|
||||
|
||||
// Delete the review
|
||||
await prisma.review.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Error deleting review:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete review' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
91
app/api/admin/reviews/route.ts
Normal file
91
app/api/admin/reviews/route.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '20')
|
||||
const search = searchParams.get('search') || ''
|
||||
const approved = searchParams.get('approved')
|
||||
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
const where: any = {}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ comment: { contains: search, mode: 'insensitive' } },
|
||||
{ user: { name: { contains: search, mode: 'insensitive' } } },
|
||||
{ product: { name: { contains: search, mode: 'insensitive' } } }
|
||||
]
|
||||
}
|
||||
|
||||
if (approved !== null && approved !== undefined) {
|
||||
where.isApproved = approved === 'true'
|
||||
}
|
||||
|
||||
const [reviews, total] = await Promise.all([
|
||||
prisma.review.findMany({
|
||||
where,
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true
|
||||
}
|
||||
},
|
||||
product: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true
|
||||
}
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
helpfulVotedBy: true,
|
||||
reportedBy: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
skip,
|
||||
take: limit
|
||||
}),
|
||||
prisma.review.count({ where })
|
||||
])
|
||||
|
||||
const reviewsWithCounts = reviews.map(review => ({
|
||||
...review,
|
||||
helpfulVotes: review._count.helpfulVotedBy,
|
||||
reportCount: review._count.reportedBy
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
reviews: reviewsWithCounts,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching admin reviews:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch reviews' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
62
app/api/admin/settings/categories/route.ts
Normal file
62
app/api/admin/settings/categories/route.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const categories = await prisma.category.findMany({
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
products: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { name: 'asc' }
|
||||
})
|
||||
|
||||
const categoriesWithCount = categories.map(category => ({
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
description: category.description,
|
||||
image: category.image,
|
||||
isActive: category.isActive,
|
||||
productCount: category._count.products
|
||||
}))
|
||||
|
||||
return NextResponse.json({ categories: categoriesWithCount })
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories:', error)
|
||||
return NextResponse.json({ categories: [] })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { name, description, image, isActive } = await request.json()
|
||||
|
||||
const category = await prisma.category.create({
|
||||
data: { name, description, image, isActive }
|
||||
})
|
||||
|
||||
return NextResponse.json(category)
|
||||
} catch (error) {
|
||||
console.error('Error creating category:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create category' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
46
app/api/admin/settings/commissions/route.ts
Normal file
46
app/api/admin/settings/commissions/route.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const settings = await prisma.commissionSettings.findMany({
|
||||
orderBy: { level: 'asc' }
|
||||
})
|
||||
|
||||
return NextResponse.json({ settings })
|
||||
} catch (error) {
|
||||
console.error('Error fetching commission settings:', error)
|
||||
return NextResponse.json({ settings: [] })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { level, percentage, isActive } = await request.json()
|
||||
|
||||
const setting = await prisma.commissionSettings.create({
|
||||
data: { level, percentage, isActive }
|
||||
})
|
||||
|
||||
return NextResponse.json(setting)
|
||||
} catch (error) {
|
||||
console.error('Error creating commission setting:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create commission setting' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
128
app/api/admin/settings/system/route.ts
Normal file
128
app/api/admin/settings/system/route.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { clearSettingsCache } from '@/lib/settings'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get or create system settings
|
||||
let settings = await prisma.systemSettings.findFirst()
|
||||
|
||||
if (!settings) {
|
||||
// Create default settings if none exist
|
||||
settings = await prisma.systemSettings.create({
|
||||
data: {
|
||||
siteName: 'Padmaaja Rasooi',
|
||||
siteDescription: 'Premium Rice Products & Quality Grains',
|
||||
supportEmail: 'support@padmaajarasooi.com',
|
||||
minimumPayout: 100,
|
||||
enableReferrals: true,
|
||||
enableCommissions: true,
|
||||
maintenanceMode: false,
|
||||
allowRegistration: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
siteName: settings.siteName,
|
||||
siteDescription: settings.siteDescription,
|
||||
supportEmail: settings.supportEmail,
|
||||
minimumPayout: settings.minimumPayout,
|
||||
enableReferrals: settings.enableReferrals,
|
||||
enableCommissions: settings.enableCommissions,
|
||||
maintenanceMode: settings.maintenanceMode,
|
||||
allowRegistration: settings.allowRegistration
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching system settings:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch system settings' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const {
|
||||
siteName,
|
||||
siteDescription,
|
||||
supportEmail,
|
||||
minimumPayout,
|
||||
enableReferrals,
|
||||
enableCommissions,
|
||||
maintenanceMode,
|
||||
allowRegistration
|
||||
} = await request.json()
|
||||
|
||||
// Get or create system settings
|
||||
let settings = await prisma.systemSettings.findFirst()
|
||||
|
||||
if (!settings) {
|
||||
// Create new settings
|
||||
settings = await prisma.systemSettings.create({
|
||||
data: {
|
||||
siteName,
|
||||
siteDescription,
|
||||
supportEmail,
|
||||
minimumPayout,
|
||||
enableReferrals,
|
||||
enableCommissions,
|
||||
maintenanceMode,
|
||||
allowRegistration
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Update existing settings
|
||||
settings = await prisma.systemSettings.update({
|
||||
where: { id: settings.id },
|
||||
data: {
|
||||
siteName,
|
||||
siteDescription,
|
||||
supportEmail,
|
||||
minimumPayout,
|
||||
enableReferrals,
|
||||
enableCommissions,
|
||||
maintenanceMode,
|
||||
allowRegistration
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Clear the settings cache so new values are fetched
|
||||
clearSettingsCache()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
settings: {
|
||||
siteName: settings.siteName,
|
||||
siteDescription: settings.siteDescription,
|
||||
supportEmail: settings.supportEmail,
|
||||
minimumPayout: settings.minimumPayout,
|
||||
enableReferrals: settings.enableReferrals,
|
||||
enableCommissions: settings.enableCommissions,
|
||||
maintenanceMode: settings.maintenanceMode,
|
||||
allowRegistration: settings.allowRegistration
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error updating system settings:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update system settings' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
76
app/api/admin/users/route.ts
Normal file
76
app/api/admin/users/route.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '10')
|
||||
const search = searchParams.get('search')
|
||||
const role = searchParams.get('role')
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
const where: any = {}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ email: { contains: search, mode: 'insensitive' } },
|
||||
]
|
||||
}
|
||||
|
||||
if (role) {
|
||||
where.role = role
|
||||
}
|
||||
|
||||
const [users, total] = await Promise.all([
|
||||
prisma.user.findMany({
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
role: true,
|
||||
isActive: true,
|
||||
joinedAt: true,
|
||||
referralCode: true,
|
||||
referrer: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
referrals: true,
|
||||
orders: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { joinedAt: 'desc' },
|
||||
}),
|
||||
prisma.user.count({ where }),
|
||||
])
|
||||
|
||||
return NextResponse.json({
|
||||
users,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Admin users API error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
3
app/api/auth/[...nextauth]/route.ts
Normal file
3
app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { handlers } from '@/auth'
|
||||
|
||||
export const { GET, POST } = handlers
|
||||
72
app/api/auth/signup/route.ts
Normal file
72
app/api/auth/signup/route.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { hash } from 'bcryptjs'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { signUpSchema } from '@/lib/validations/auth'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { name, email, password, phone, referralCode, role } = signUpSchema.parse(body)
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ error: 'User already exists' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await hash(password, 12)
|
||||
|
||||
// Find referrer if referral code is provided
|
||||
let referrerId = null
|
||||
if (referralCode) {
|
||||
const referrer = await prisma.user.findUnique({
|
||||
where: { referralCode },
|
||||
})
|
||||
if (referrer) {
|
||||
referrerId = referrer.id
|
||||
}
|
||||
}
|
||||
|
||||
// Create user
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
name,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
phone,
|
||||
role: role || 'CUSTOMER',
|
||||
referrerId,
|
||||
},
|
||||
})
|
||||
|
||||
// Create wallet for members
|
||||
if (role === 'MEMBER') {
|
||||
await prisma.wallet.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
balance: 0,
|
||||
totalEarnings: 0,
|
||||
totalWithdrawn: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: 'User created successfully', userId: user.id },
|
||||
{ status: 201 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Signup error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
105
app/api/categories/route.ts
Normal file
105
app/api/categories/route.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { categorySchema } from '@/lib/validations/product'
|
||||
import { DatabaseOptimizer } from '@/lib/database-optimizer'
|
||||
|
||||
export async function GET() {
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
// Cache key for categories
|
||||
const cacheKey = 'categories_navigation_with_counts'
|
||||
|
||||
// Try to get from cache first
|
||||
const cached = await DatabaseOptimizer.getCachedData(cacheKey)
|
||||
if (cached) {
|
||||
return NextResponse.json({
|
||||
...cached,
|
||||
_performance: {
|
||||
responseTime: Date.now() - startTime,
|
||||
cached: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Get categories with product counts for better navigation
|
||||
const categories = await DatabaseOptimizer.executeOptimizedQuery(
|
||||
'categories_with_product_counts',
|
||||
async () => {
|
||||
const cats = await prisma.category.findMany({
|
||||
where: { isActive: true },
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
products: {
|
||||
where: {
|
||||
isActive: true,
|
||||
stock: { gt: 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
})
|
||||
|
||||
// Add SEO-friendly data
|
||||
return cats.map(cat => ({
|
||||
...cat,
|
||||
productCount: cat._count.products,
|
||||
hasProducts: cat._count.products > 0,
|
||||
_count: undefined // Remove from response
|
||||
}))
|
||||
},
|
||||
1800 // Cache for 30 minutes
|
||||
)
|
||||
|
||||
const responseData = {
|
||||
categories,
|
||||
seo: {
|
||||
totalCategories: categories.length,
|
||||
categoriesWithProducts: categories.filter(c => c.hasProducts).length
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the response
|
||||
await DatabaseOptimizer.setCachedData(cacheKey, responseData, 1800)
|
||||
|
||||
return NextResponse.json({
|
||||
...responseData,
|
||||
_performance: {
|
||||
responseTime: Date.now() - startTime,
|
||||
cached: false
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Categories API error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const data = categorySchema.parse(body)
|
||||
|
||||
const category = await prisma.category.create({
|
||||
data,
|
||||
})
|
||||
|
||||
// Invalidate category caches after creation
|
||||
DatabaseOptimizer.invalidateCache('categories')
|
||||
DatabaseOptimizer.invalidateCache('category_stats')
|
||||
|
||||
return NextResponse.json(category, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Create category error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
69
app/api/commissions/route.ts
Normal file
69
app/api/commissions/route.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const url = new URL(request.url)
|
||||
const page = parseInt(url.searchParams.get('page') || '1')
|
||||
const limit = parseInt(url.searchParams.get('limit') || '50')
|
||||
const status = url.searchParams.get('status')
|
||||
const type = url.searchParams.get('type')
|
||||
const level = url.searchParams.get('level')
|
||||
|
||||
const where: any = { userId: session.user.id }
|
||||
|
||||
if (status && status !== 'all') {
|
||||
where.status = status
|
||||
}
|
||||
|
||||
if (type && type !== 'all') {
|
||||
where.type = type
|
||||
}
|
||||
|
||||
if (level && level !== 'all') {
|
||||
where.level = parseInt(level)
|
||||
}
|
||||
|
||||
const commissions = await prisma.commission.findMany({
|
||||
where,
|
||||
include: {
|
||||
fromUser: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
skip: (page - 1) * limit,
|
||||
take: limit
|
||||
})
|
||||
|
||||
const total = await prisma.commission.count({ where })
|
||||
|
||||
return NextResponse.json({
|
||||
commissions,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching commissions:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch commissions' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
95
app/api/commissions/stats/route.ts
Normal file
95
app/api/commissions/stats/route.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
|
||||
// Get total earnings
|
||||
const totalEarningsResult = await prisma.commission.aggregate({
|
||||
where: {
|
||||
userId,
|
||||
status: { in: ['APPROVED', 'PAID'] }
|
||||
},
|
||||
_sum: {
|
||||
amount: true
|
||||
}
|
||||
})
|
||||
|
||||
// Get pending amount
|
||||
const pendingAmountResult = await prisma.commission.aggregate({
|
||||
where: {
|
||||
userId,
|
||||
status: 'PENDING'
|
||||
},
|
||||
_sum: {
|
||||
amount: true
|
||||
}
|
||||
})
|
||||
|
||||
// Get this month earnings
|
||||
const now = new Date()
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||
|
||||
const thisMonthResult = await prisma.commission.aggregate({
|
||||
where: {
|
||||
userId,
|
||||
status: { in: ['APPROVED', 'PAID'] },
|
||||
createdAt: {
|
||||
gte: startOfMonth
|
||||
}
|
||||
},
|
||||
_sum: {
|
||||
amount: true
|
||||
}
|
||||
})
|
||||
|
||||
// Get total commissions count
|
||||
const totalCommissions = await prisma.commission.count({
|
||||
where: { userId }
|
||||
})
|
||||
|
||||
// Get earnings by level
|
||||
const byLevel = await prisma.commission.groupBy({
|
||||
by: ['level'],
|
||||
where: {
|
||||
userId,
|
||||
status: { in: ['APPROVED', 'PAID'] }
|
||||
},
|
||||
_sum: {
|
||||
amount: true
|
||||
},
|
||||
_count: {
|
||||
id: true
|
||||
},
|
||||
orderBy: {
|
||||
level: 'asc'
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
totalEarnings: totalEarningsResult._sum.amount || 0,
|
||||
pendingAmount: pendingAmountResult._sum.amount || 0,
|
||||
thisMonthEarnings: thisMonthResult._sum.amount || 0,
|
||||
totalCommissions,
|
||||
byLevel: byLevel.map(item => ({
|
||||
level: item.level,
|
||||
amount: item._sum.amount || 0,
|
||||
count: item._count.id
|
||||
}))
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching commission stats:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch commission stats' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
56
app/api/dashboard/commissions/route.ts
Normal file
56
app/api/dashboard/commissions/route.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '10')
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
const [commissions, total] = await Promise.all([
|
||||
prisma.commission.findMany({
|
||||
where: { userId: session.user.id },
|
||||
include: {
|
||||
fromUser: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: limit
|
||||
}),
|
||||
prisma.commission.count({ where: { userId: session.user.id } })
|
||||
])
|
||||
|
||||
return NextResponse.json({
|
||||
commissions: commissions.map(commission => ({
|
||||
id: commission.id,
|
||||
amount: commission.amount,
|
||||
level: commission.level,
|
||||
type: commission.type,
|
||||
status: commission.status,
|
||||
createdAt: commission.createdAt,
|
||||
fromUser: commission.fromUser
|
||||
})),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching commissions:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch commissions' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
171
app/api/dashboard/genealogy/route.ts
Normal file
171
app/api/dashboard/genealogy/route.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
interface TeamMember {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
joinedAt: string
|
||||
totalEarnings: number
|
||||
directReferrals: number
|
||||
level: number
|
||||
children?: TeamMember[]
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
|
||||
// Build genealogy tree
|
||||
const genealogyTree = await buildGenealogyTree(userId, 0)
|
||||
|
||||
// Calculate stats
|
||||
const teamSize = await calculateTeamSize(userId)
|
||||
const totalVolume = await calculateTeamVolume(userId)
|
||||
const levels = await calculateNetworkLevels(userId)
|
||||
|
||||
return NextResponse.json({
|
||||
user: genealogyTree,
|
||||
teamSize,
|
||||
totalVolume,
|
||||
levels
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching genealogy data:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch genealogy data' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function buildGenealogyTree(userId: string, level: number): Promise<TeamMember> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
wallet: true,
|
||||
referrals: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
joinedAt: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found')
|
||||
}
|
||||
|
||||
// Get direct referrals count
|
||||
const directReferrals = await prisma.user.count({
|
||||
where: { referrerId: userId }
|
||||
})
|
||||
|
||||
const teamMember: TeamMember = {
|
||||
id: user.id,
|
||||
name: user.name || 'Unknown',
|
||||
email: user.email,
|
||||
joinedAt: user.joinedAt.toISOString(),
|
||||
totalEarnings: user.wallet?.totalEarnings || 0,
|
||||
directReferrals,
|
||||
level,
|
||||
children: []
|
||||
}
|
||||
|
||||
// Recursively build children (limit to 5 levels to prevent infinite recursion)
|
||||
if (level < 5 && user.referrals.length > 0) {
|
||||
for (const referral of user.referrals) {
|
||||
const childTree = await buildGenealogyTree(referral.id, level + 1)
|
||||
teamMember.children!.push(childTree)
|
||||
}
|
||||
}
|
||||
|
||||
return teamMember
|
||||
}
|
||||
|
||||
async function calculateTeamSize(userId: string): Promise<number> {
|
||||
const getAllTeamMembers = async (id: string): Promise<string[]> => {
|
||||
const directReferrals = await prisma.user.findMany({
|
||||
where: { referrerId: id },
|
||||
select: { id: true }
|
||||
})
|
||||
|
||||
let allMembers = directReferrals.map(r => r.id)
|
||||
|
||||
for (const referral of directReferrals) {
|
||||
const subTeam = await getAllTeamMembers(referral.id)
|
||||
allMembers = [...allMembers, ...subTeam]
|
||||
}
|
||||
|
||||
return allMembers
|
||||
}
|
||||
|
||||
const teamMembers = await getAllTeamMembers(userId)
|
||||
return teamMembers.length
|
||||
}
|
||||
|
||||
async function calculateTeamVolume(userId: string): Promise<number> {
|
||||
const getAllTeamMembers = async (id: string): Promise<string[]> => {
|
||||
const directReferrals = await prisma.user.findMany({
|
||||
where: { referrerId: id },
|
||||
select: { id: true }
|
||||
})
|
||||
|
||||
let allMembers = directReferrals.map(r => r.id)
|
||||
|
||||
for (const referral of directReferrals) {
|
||||
const subTeam = await getAllTeamMembers(referral.id)
|
||||
allMembers = [...allMembers, ...subTeam]
|
||||
}
|
||||
|
||||
return allMembers
|
||||
}
|
||||
|
||||
const teamMembers = await getAllTeamMembers(userId)
|
||||
|
||||
if (teamMembers.length === 0) return 0
|
||||
|
||||
const volume = await prisma.order.aggregate({
|
||||
where: {
|
||||
userId: { in: teamMembers },
|
||||
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] }
|
||||
},
|
||||
_sum: { total: true }
|
||||
})
|
||||
|
||||
return volume._sum.total || 0
|
||||
}
|
||||
|
||||
async function calculateNetworkLevels(userId: string): Promise<number> {
|
||||
const getMaxLevel = async (id: string, currentLevel: number): Promise<number> => {
|
||||
const directReferrals = await prisma.user.findMany({
|
||||
where: { referrerId: id },
|
||||
select: { id: true }
|
||||
})
|
||||
|
||||
if (directReferrals.length === 0) {
|
||||
return currentLevel
|
||||
}
|
||||
|
||||
let maxLevel = currentLevel + 1
|
||||
|
||||
for (const referral of directReferrals) {
|
||||
const level = await getMaxLevel(referral.id, currentLevel + 1)
|
||||
maxLevel = Math.max(maxLevel, level)
|
||||
}
|
||||
|
||||
return maxLevel
|
||||
}
|
||||
|
||||
return await getMaxLevel(userId, 0)
|
||||
}
|
||||
73
app/api/dashboard/payouts/route.ts
Normal file
73
app/api/dashboard/payouts/route.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { amount, bankDetails } = await request.json()
|
||||
|
||||
// Check wallet balance
|
||||
const wallet = await prisma.wallet.findUnique({
|
||||
where: { userId: session.user.id }
|
||||
})
|
||||
|
||||
if (!wallet || wallet.balance < amount) {
|
||||
return NextResponse.json({ error: 'Insufficient balance' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Create payout request
|
||||
const payout = await prisma.payout.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
amount,
|
||||
bankDetails,
|
||||
status: 'PENDING'
|
||||
}
|
||||
})
|
||||
|
||||
// Update wallet balance (deduct the requested amount)
|
||||
await prisma.wallet.update({
|
||||
where: { userId: session.user.id },
|
||||
data: {
|
||||
balance: { decrement: amount }
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json(payout)
|
||||
} catch (error) {
|
||||
console.error('Error creating payout request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create payout request' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const payouts = await prisma.payout.findMany({
|
||||
where: { userId: session.user.id },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
|
||||
return NextResponse.json(payouts)
|
||||
} catch (error) {
|
||||
console.error('Error fetching payouts:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch payouts' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
80
app/api/dashboard/profile/[id]/route.ts
Normal file
80
app/api/dashboard/profile/[id]/route.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { CommissionService } from '@/lib/commission'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
// Get user profile
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
phone: true,
|
||||
address: true,
|
||||
joinedAt: true,
|
||||
role: true,
|
||||
referralCode: true,
|
||||
isActive: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Get team stats
|
||||
const stats = await CommissionService.getTeamStats(id)
|
||||
|
||||
// Get wallet info
|
||||
const wallet = await prisma.wallet.findUnique({
|
||||
where: { userId: id }
|
||||
})
|
||||
|
||||
// Get recent commissions
|
||||
const recentCommissions = await prisma.commission.findMany({
|
||||
where: { userId: id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 5,
|
||||
select: {
|
||||
id: true,
|
||||
amount: true,
|
||||
level: true,
|
||||
type: true,
|
||||
status: true,
|
||||
createdAt: true
|
||||
}
|
||||
})
|
||||
|
||||
const profile = {
|
||||
...user,
|
||||
stats: {
|
||||
...stats,
|
||||
totalEarnings: wallet?.totalEarnings || 0,
|
||||
walletBalance: wallet?.balance || 0
|
||||
},
|
||||
recentCommissions
|
||||
}
|
||||
|
||||
return NextResponse.json(profile)
|
||||
} catch (error) {
|
||||
console.error('Error fetching user profile:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch profile' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
70
app/api/dashboard/referrals/route.ts
Normal file
70
app/api/dashboard/referrals/route.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get user's direct referrals
|
||||
const referrals = await prisma.user.findMany({
|
||||
where: {
|
||||
referrerId: session.user.id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
phone: true,
|
||||
joinedAt: true,
|
||||
isActive: true,
|
||||
orders: {
|
||||
where: {
|
||||
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] }
|
||||
},
|
||||
select: {
|
||||
total: true
|
||||
}
|
||||
},
|
||||
// Get commissions earned from this referral
|
||||
commissionsFrom: {
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
status: { in: ['APPROVED', 'PAID'] }
|
||||
},
|
||||
select: {
|
||||
amount: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const formattedReferrals = referrals.map(referral => ({
|
||||
id: referral.id,
|
||||
name: referral.name || 'User',
|
||||
email: referral.email,
|
||||
phone: referral.phone,
|
||||
joinedAt: referral.joinedAt,
|
||||
isActive: referral.isActive,
|
||||
totalOrders: referral.orders.length,
|
||||
totalSpent: referral.orders.reduce((sum, order) => sum + order.total, 0),
|
||||
commissionEarned: referral.commissionsFrom.reduce((sum, comm) => sum + comm.amount, 0),
|
||||
status: referral.isActive ? 'ACTIVE' : 'INACTIVE'
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
referrals: formattedReferrals
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching referrals:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch referrals' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
89
app/api/dashboard/referrals/stats/route.ts
Normal file
89
app/api/dashboard/referrals/stats/route.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
|
||||
// Get total referrals
|
||||
const totalReferrals = await prisma.user.count({
|
||||
where: { referrerId: userId }
|
||||
})
|
||||
|
||||
// Get active referrals
|
||||
const activeReferrals = await prisma.user.count({
|
||||
where: {
|
||||
referrerId: userId,
|
||||
isActive: true
|
||||
}
|
||||
})
|
||||
|
||||
// Get pending referrals
|
||||
const pendingReferrals = await prisma.user.count({
|
||||
where: {
|
||||
referrerId: userId,
|
||||
isActive: false
|
||||
}
|
||||
})
|
||||
|
||||
// Get total commission earned from referrals
|
||||
const totalCommissionResult = await prisma.commission.aggregate({
|
||||
where: {
|
||||
userId,
|
||||
status: { in: ['APPROVED', 'PAID'] }
|
||||
},
|
||||
_sum: {
|
||||
amount: true
|
||||
}
|
||||
})
|
||||
|
||||
// Get this month's stats
|
||||
const now = new Date()
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||
|
||||
const thisMonthReferrals = await prisma.user.count({
|
||||
where: {
|
||||
referrerId: userId,
|
||||
joinedAt: {
|
||||
gte: startOfMonth
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const thisMonthCommissionResult = await prisma.commission.aggregate({
|
||||
where: {
|
||||
userId,
|
||||
status: { in: ['APPROVED', 'PAID'] },
|
||||
createdAt: {
|
||||
gte: startOfMonth
|
||||
}
|
||||
},
|
||||
_sum: {
|
||||
amount: true
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
totalReferrals,
|
||||
activeReferrals,
|
||||
pendingReferrals,
|
||||
totalCommissionEarned: totalCommissionResult._sum.amount || 0,
|
||||
thisMonthReferrals,
|
||||
thisMonthCommission: thisMonthCommissionResult._sum.amount || 0
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching referral stats:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch referral stats' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
159
app/api/dashboard/reports/route.ts
Normal file
159
app/api/dashboard/reports/route.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const range = parseInt(searchParams.get('range') || '30')
|
||||
const type = searchParams.get('type') || 'all'
|
||||
|
||||
const userId = session.user.id
|
||||
const startDate = new Date()
|
||||
startDate.setDate(startDate.getDate() - range)
|
||||
|
||||
// Get commission data
|
||||
const commissions = await prisma.commission.findMany({
|
||||
where: {
|
||||
userId,
|
||||
createdAt: { gte: startDate }
|
||||
},
|
||||
include: {
|
||||
fromUser: {
|
||||
select: { name: true, email: true }
|
||||
}
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
|
||||
const totalCommissions = commissions.reduce((sum, c) => sum + c.amount, 0)
|
||||
|
||||
// Calculate commission growth
|
||||
const previousPeriodStart = new Date(startDate)
|
||||
previousPeriodStart.setDate(previousPeriodStart.getDate() - range)
|
||||
|
||||
const previousCommissions = await prisma.commission.findMany({
|
||||
where: {
|
||||
userId,
|
||||
createdAt: {
|
||||
gte: previousPeriodStart,
|
||||
lt: startDate
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const previousTotal = previousCommissions.reduce((sum, c) => sum + c.amount, 0)
|
||||
const growth = previousTotal > 0 ? ((totalCommissions - previousTotal) / previousTotal) * 100 : 0
|
||||
|
||||
// Commission by level
|
||||
const commissionsByLevel = commissions.reduce((acc, c) => {
|
||||
const existing = acc.find(item => item.level === c.level)
|
||||
if (existing) {
|
||||
existing.amount += c.amount
|
||||
existing.count += 1
|
||||
} else {
|
||||
acc.push({ level: c.level, amount: c.amount, count: 1 })
|
||||
}
|
||||
return acc
|
||||
}, [] as { level: number; amount: number; count: number }[])
|
||||
|
||||
// Get team data
|
||||
const directReferrals = await prisma.user.findMany({
|
||||
where: { referrerId: userId },
|
||||
select: {
|
||||
id: true,
|
||||
joinedAt: true,
|
||||
orders: {
|
||||
where: {
|
||||
createdAt: { gte: startDate },
|
||||
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Get all team members recursively
|
||||
const getAllTeamMembers = async (id: string): Promise<string[]> => {
|
||||
const refs = await prisma.user.findMany({
|
||||
where: { referrerId: id },
|
||||
select: { id: true }
|
||||
})
|
||||
|
||||
let allMembers = refs.map(r => r.id)
|
||||
|
||||
for (const ref of refs) {
|
||||
const subTeam = await getAllTeamMembers(ref.id)
|
||||
allMembers = [...allMembers, ...subTeam]
|
||||
}
|
||||
|
||||
return allMembers
|
||||
}
|
||||
|
||||
const allTeamMemberIds = await getAllTeamMembers(userId)
|
||||
|
||||
// Team metrics
|
||||
const activeMembers = directReferrals.filter(r => r.orders.length > 0).length
|
||||
const newThisMonth = directReferrals.filter(r =>
|
||||
new Date(r.joinedAt) >= new Date(new Date().getFullYear(), new Date().getMonth(), 1)
|
||||
).length
|
||||
|
||||
// Sales data
|
||||
const userOrders = await prisma.order.findMany({
|
||||
where: {
|
||||
userId,
|
||||
createdAt: { gte: startDate },
|
||||
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] }
|
||||
}
|
||||
})
|
||||
|
||||
const teamOrders = await prisma.order.findMany({
|
||||
where: {
|
||||
userId: { in: allTeamMemberIds },
|
||||
createdAt: { gte: startDate },
|
||||
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] }
|
||||
}
|
||||
})
|
||||
|
||||
const personalVolume = userOrders.reduce((sum, o) => sum + o.total, 0)
|
||||
const teamVolume = teamOrders.reduce((sum, o) => sum + o.total, 0)
|
||||
|
||||
const reportData = {
|
||||
commissions: {
|
||||
total: totalCommissions,
|
||||
thisMonth: commissions.filter(c =>
|
||||
new Date(c.createdAt) >= new Date(new Date().getFullYear(), new Date().getMonth(), 1)
|
||||
).reduce((sum, c) => sum + c.amount, 0),
|
||||
lastMonth: previousTotal,
|
||||
growth,
|
||||
byLevel: commissionsByLevel.sort((a, b) => a.level - b.level),
|
||||
recent: commissions.slice(0, 10)
|
||||
},
|
||||
team: {
|
||||
totalMembers: allTeamMemberIds.length,
|
||||
activeMembers,
|
||||
newThisMonth,
|
||||
byLevel: [{ level: 1, count: directReferrals.length }] // Simplified for now
|
||||
},
|
||||
sales: {
|
||||
totalVolume: personalVolume + teamVolume,
|
||||
personalVolume,
|
||||
teamVolume,
|
||||
ordersCount: userOrders.length + teamOrders.length
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(reportData)
|
||||
} catch (error) {
|
||||
console.error('Error fetching reports:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch reports' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
119
app/api/dashboard/route.ts
Normal file
119
app/api/dashboard/route.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
|
||||
// Get user's wallet
|
||||
const wallet = await prisma.wallet.findUnique({
|
||||
where: { userId }
|
||||
})
|
||||
|
||||
// Get commission stats
|
||||
const [totalEarnings, pendingCommissions, thisMonthEarnings] = await Promise.all([
|
||||
prisma.commission.aggregate({
|
||||
where: {
|
||||
userId,
|
||||
status: { in: ['APPROVED', 'PAID'] }
|
||||
},
|
||||
_sum: { amount: true }
|
||||
}),
|
||||
prisma.commission.aggregate({
|
||||
where: {
|
||||
userId,
|
||||
status: 'PENDING'
|
||||
},
|
||||
_sum: { amount: true }
|
||||
}),
|
||||
prisma.commission.aggregate({
|
||||
where: {
|
||||
userId,
|
||||
status: { in: ['APPROVED', 'PAID'] },
|
||||
createdAt: {
|
||||
gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1)
|
||||
}
|
||||
},
|
||||
_sum: { amount: true }
|
||||
})
|
||||
])
|
||||
|
||||
// Get referral stats
|
||||
const [totalReferrals, activeReferrals] = await Promise.all([
|
||||
prisma.user.count({
|
||||
where: { referrerId: userId }
|
||||
}),
|
||||
prisma.user.count({
|
||||
where: {
|
||||
referrerId: userId,
|
||||
isActive: true
|
||||
}
|
||||
})
|
||||
])
|
||||
|
||||
// Get order stats
|
||||
const totalOrders = await prisma.order.count({
|
||||
where: { userId }
|
||||
})
|
||||
|
||||
// Get recent commissions
|
||||
const recentCommissions = await prisma.commission.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
fromUser: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 5
|
||||
})
|
||||
|
||||
// Get recent orders
|
||||
const recentOrders = await prisma.order.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
orderItems: {
|
||||
include: {
|
||||
product: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 5
|
||||
})
|
||||
|
||||
const stats = {
|
||||
totalEarnings: totalEarnings._sum.amount || 0,
|
||||
pendingCommissions: pendingCommissions._sum.amount || 0,
|
||||
thisMonthEarnings: thisMonthEarnings._sum.amount || 0,
|
||||
currentBalance: wallet?.balance || 0,
|
||||
totalReferrals,
|
||||
activeReferrals,
|
||||
totalOrders,
|
||||
recentCommissions,
|
||||
recentOrders
|
||||
}
|
||||
|
||||
return NextResponse.json({ stats })
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard stats:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch dashboard stats' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
48
app/api/dashboard/stats/route.ts
Normal file
48
app/api/dashboard/stats/route.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { CommissionService } from '@/lib/commission'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
|
||||
// Get team stats
|
||||
const teamStats = await CommissionService.getTeamStats(userId)
|
||||
|
||||
// Get wallet info
|
||||
const wallet = await prisma.wallet.findUnique({
|
||||
where: { userId }
|
||||
})
|
||||
|
||||
// Get pending commissions
|
||||
const pendingCommissions = await prisma.commission.aggregate({
|
||||
where: {
|
||||
userId,
|
||||
status: 'PENDING'
|
||||
},
|
||||
_sum: { amount: true }
|
||||
})
|
||||
|
||||
const stats = {
|
||||
...teamStats,
|
||||
totalEarnings: wallet?.totalEarnings || 0,
|
||||
walletBalance: wallet?.balance || 0,
|
||||
pendingCommissions: pendingCommissions._sum.amount || 0
|
||||
}
|
||||
|
||||
return NextResponse.json(stats)
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard stats:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch stats' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
100
app/api/dashboard/team/route.ts
Normal file
100
app/api/dashboard/team/route.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
|
||||
// Get direct referrals
|
||||
const directReferrals = await prisma.user.findMany({
|
||||
where: { referrerId: userId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
role: true,
|
||||
joinedAt: true,
|
||||
referrals: {
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { joinedAt: 'desc' }
|
||||
})
|
||||
|
||||
// Get team statistics by levels
|
||||
const getAllTeamMembers = async (id: string, level: number = 1, maxLevel: number = 5): Promise<string[]> => {
|
||||
if (level > maxLevel) return []
|
||||
|
||||
const refs = await prisma.user.findMany({
|
||||
where: { referrerId: id },
|
||||
select: { id: true }
|
||||
})
|
||||
|
||||
let allMembers = refs.map(r => r.id)
|
||||
|
||||
if (level < maxLevel) {
|
||||
for (const ref of refs) {
|
||||
const subTeam = await getAllTeamMembers(ref.id, level + 1, maxLevel)
|
||||
allMembers = [...allMembers, ...subTeam]
|
||||
}
|
||||
}
|
||||
|
||||
return allMembers
|
||||
}
|
||||
|
||||
// Calculate team size per level
|
||||
const levels = []
|
||||
for (let level = 1; level <= 5; level++) {
|
||||
if (level === 1) {
|
||||
levels.push({ level, count: directReferrals.length })
|
||||
} else {
|
||||
// Get members at specific level
|
||||
const getLevelMembers = async (parentIds: string[], targetLevel: number, currentLevel: number = 1): Promise<string[]> => {
|
||||
if (currentLevel === targetLevel) return parentIds
|
||||
|
||||
const nextLevel = await prisma.user.findMany({
|
||||
where: { referrerId: { in: parentIds } },
|
||||
select: { id: true }
|
||||
})
|
||||
|
||||
if (nextLevel.length === 0 || currentLevel >= targetLevel) return []
|
||||
|
||||
return getLevelMembers(nextLevel.map(u => u.id), targetLevel, currentLevel + 1)
|
||||
}
|
||||
|
||||
const levelMemberIds = await getLevelMembers([userId], level)
|
||||
levels.push({ level, count: levelMemberIds.length })
|
||||
}
|
||||
}
|
||||
|
||||
const allTeamMemberIds = await getAllTeamMembers(userId)
|
||||
|
||||
const teamData = {
|
||||
directReferrals: directReferrals.map(ref => ({
|
||||
...ref,
|
||||
referrals: ref.referrals || []
|
||||
})),
|
||||
teamStats: {
|
||||
totalTeamSize: allTeamMemberIds.length,
|
||||
levels
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(teamData)
|
||||
} catch (error) {
|
||||
console.error('Error fetching team data:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch team data' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
71
app/api/dashboard/wallet/route.ts
Normal file
71
app/api/dashboard/wallet/route.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
|
||||
// Get wallet info
|
||||
const wallet = await prisma.wallet.findUnique({
|
||||
where: { userId }
|
||||
})
|
||||
|
||||
// Mock transaction history (you can implement proper transaction tracking)
|
||||
const commissions = await prisma.commission.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 20,
|
||||
include: {
|
||||
fromUser: {
|
||||
select: { name: true }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const payouts = await prisma.payout.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 20
|
||||
})
|
||||
|
||||
// Combine and format transactions
|
||||
const transactions = [
|
||||
...commissions.map(commission => ({
|
||||
id: commission.id,
|
||||
type: 'COMMISSION' as const,
|
||||
amount: commission.amount,
|
||||
description: `Level ${commission.level} commission from ${commission.fromUser.name}`,
|
||||
status: commission.status,
|
||||
createdAt: commission.createdAt
|
||||
})),
|
||||
...payouts.map(payout => ({
|
||||
id: payout.id,
|
||||
type: 'PAYOUT' as const,
|
||||
amount: payout.amount,
|
||||
description: 'Withdrawal request',
|
||||
status: payout.status,
|
||||
createdAt: payout.createdAt
|
||||
}))
|
||||
].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
|
||||
return NextResponse.json({
|
||||
balance: wallet?.balance || 0,
|
||||
totalEarnings: wallet?.totalEarnings || 0,
|
||||
totalWithdrawn: wallet?.totalWithdrawn || 0,
|
||||
transactions
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching wallet data:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch wallet data' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
105
app/api/forms/[id]/route.ts
Normal file
105
app/api/forms/[id]/route.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { z } from 'zod'
|
||||
|
||||
const UpdateFormSchema = z.object({
|
||||
status: z.enum(['new', 'in_progress', 'resolved', 'closed'])
|
||||
})
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const formResponse = await prisma.formResponse.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
if (!formResponse) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Form response not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: formResponse
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching form response:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Failed to fetch form response' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const validatedData = UpdateFormSchema.parse(body)
|
||||
|
||||
const formResponse = await prisma.formResponse.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: validatedData.status,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: formResponse
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating form response:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'Validation error',
|
||||
errors: error.errors
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Failed to update form response' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
await prisma.formResponse.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Form response deleted successfully'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting form response:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Failed to delete form response' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
168
app/api/forms/route.ts
Normal file
168
app/api/forms/route.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { z } from 'zod'
|
||||
import { Prisma } from '@prisma/client'
|
||||
|
||||
// CORS headers
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
}
|
||||
|
||||
// Validation schema for form submissions
|
||||
const FormSubmissionSchema = z.object({
|
||||
formId: z.string().min(1, 'Form ID is required'), // e.g., "contact", "partnership", "bulk_inquiry"
|
||||
name: z.string().min(2, 'Name must be at least 2 characters'),
|
||||
email: z.string().email('Invalid email address'),
|
||||
phone: z.string().optional(),
|
||||
subject: z.string().optional(),
|
||||
message: z.string().min(10, 'Message must be at least 10 characters'),
|
||||
inquiryType: z.string().optional(),
|
||||
company: z.string().optional(),
|
||||
formSource: z.string().optional(), // Optional field to track which form page it came from
|
||||
// Allow additional fields that will be stored in JSON
|
||||
}).passthrough()
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
headers: corsHeaders,
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Validate the form data
|
||||
const validatedData = FormSubmissionSchema.parse(body)
|
||||
|
||||
// Extract formId from the data
|
||||
const { formId, ...formData } = validatedData
|
||||
|
||||
// Get client information
|
||||
const forwarded = request.headers.get('x-forwarded-for')
|
||||
const ipAddress = forwarded ? forwarded.split(',')[0] :
|
||||
request.headers.get('x-real-ip') ||
|
||||
'unknown'
|
||||
|
||||
const userAgent = request.headers.get('user-agent') || 'unknown'
|
||||
const referrer = request.headers.get('referer') || null
|
||||
|
||||
// Prepare metadata
|
||||
const metadata = {
|
||||
ipAddress,
|
||||
userAgent,
|
||||
referrer,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
|
||||
// Save to database with new simplified schema
|
||||
const formResponse = await prisma.formResponse.create({
|
||||
data: {
|
||||
formId,
|
||||
data: formData as Prisma.JsonObject, // Store all form data as JSON with proper Prisma type
|
||||
metadata: metadata as Prisma.JsonObject, // Cast metadata with proper Prisma type
|
||||
status: 'new'
|
||||
}
|
||||
})
|
||||
|
||||
// Send email notification (optional)
|
||||
// You can add email sending logic here using nodemailer or your preferred service
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: 'Form submitted successfully',
|
||||
id: formResponse.id
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: corsHeaders
|
||||
}
|
||||
)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'Validation error',
|
||||
errors: error.errors
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
headers: corsHeaders
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
headers: corsHeaders
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// This endpoint could be used for admin to fetch form submissions
|
||||
// Add authentication/authorization logic here
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const status = searchParams.get('status')
|
||||
const formId = searchParams.get('formId') // Changed from formType to formId
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '10')
|
||||
|
||||
const where: any = {}
|
||||
|
||||
if (status) {
|
||||
where.status = status.toLowerCase() // Changed to lowercase to match new schema
|
||||
}
|
||||
|
||||
if (formId) {
|
||||
where.formId = formId
|
||||
}
|
||||
|
||||
const [forms, total] = await Promise.all([
|
||||
prisma.formResponse.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.formResponse.count({ where })
|
||||
])
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: forms,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching forms:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'Failed to fetch forms'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
109
app/api/inquiries/route.ts
Normal file
109
app/api/inquiries/route.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { emailService } from '@/lib/email'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Validate required fields
|
||||
const requiredFields = [
|
||||
'companyName', 'contactPerson', 'designation', 'email', 'phone',
|
||||
'businessType', 'address', 'quantityRequired', 'deliveryLocation', 'message'
|
||||
]
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (!body[field]) {
|
||||
return NextResponse.json(
|
||||
{ error: `Missing required field: ${field}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare form data for database
|
||||
const formData = {
|
||||
formId: 'b2b_inquiry',
|
||||
data: {
|
||||
// Company Information
|
||||
companyName: body.companyName,
|
||||
contactPerson: body.contactPerson,
|
||||
designation: body.designation,
|
||||
email: body.email,
|
||||
phone: body.phone,
|
||||
businessType: body.businessType,
|
||||
gstNumber: body.gstNumber || null,
|
||||
address: body.address,
|
||||
|
||||
// Product Information (if applicable)
|
||||
productId: body.productId || null,
|
||||
productName: body.productName || null,
|
||||
productCategory: body.productCategory || null,
|
||||
productPrice: body.productPrice || null,
|
||||
|
||||
// Requirements
|
||||
quantityRequired: body.quantityRequired,
|
||||
quantityUnit: body.quantityUnit || 'tons',
|
||||
deliveryLocation: body.deliveryLocation,
|
||||
expectedDeliveryDate: body.expectedDeliveryDate || null,
|
||||
|
||||
// Additional Information
|
||||
message: body.message,
|
||||
hearAboutUs: body.hearAboutUs || null,
|
||||
|
||||
// Metadata
|
||||
submissionType: 'b2b_inquiry',
|
||||
submittedAt: body.submittedAt || new Date().toISOString(),
|
||||
userAgent: request.headers.get('user-agent') || null,
|
||||
ipAddress: request.headers.get('x-forwarded-for') ||
|
||||
request.headers.get('x-real-ip') ||
|
||||
'unknown'
|
||||
},
|
||||
status: 'new',
|
||||
metadata: {
|
||||
source: 'website_inquiry_form',
|
||||
type: 'b2b_bulk_inquiry',
|
||||
priority: 'high'
|
||||
}
|
||||
}
|
||||
|
||||
// Save to database
|
||||
const formResponse = await prisma.formResponse.create({
|
||||
data: formData
|
||||
})
|
||||
|
||||
// Send email notifications
|
||||
try {
|
||||
// Send confirmation email to customer
|
||||
await emailService.sendB2BInquiryConfirmation({
|
||||
customerEmail: body.email,
|
||||
customerName: body.contactPerson,
|
||||
companyName: body.companyName,
|
||||
inquiryId: formResponse.id,
|
||||
productName: body.productName
|
||||
})
|
||||
|
||||
// Send notification to admin
|
||||
await emailService.sendB2BInquiryAdminNotification({
|
||||
inquiryData: body,
|
||||
inquiryId: formResponse.id
|
||||
})
|
||||
} catch (emailError) {
|
||||
console.error('Email notification failed:', emailError)
|
||||
// Don't fail the API call if email fails
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Inquiry submitted successfully',
|
||||
inquiryId: formResponse.id
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing B2B inquiry:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to process inquiry. Please try again.' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
24
app/api/maintenance-check/route.ts
Normal file
24
app/api/maintenance-check/route.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { auth } from '@/auth'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Get system settings
|
||||
const settings = await prisma.systemSettings.findFirst()
|
||||
|
||||
// Check if user is admin
|
||||
const session = await auth()
|
||||
const isAdmin = session?.user?.role === 'ADMIN'
|
||||
|
||||
return NextResponse.json({
|
||||
maintenanceMode: (settings?.maintenanceMode || false) && !isAdmin
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error checking maintenance mode:', error)
|
||||
// Return false if there's an error to prevent blocking access
|
||||
return NextResponse.json({
|
||||
maintenanceMode: false
|
||||
})
|
||||
}
|
||||
}
|
||||
102
app/api/order-confirmation/[id]/route.ts
Normal file
102
app/api/order-confirmation/[id]/route.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: orderId } = await params
|
||||
|
||||
const order = await prisma.order.findFirst({
|
||||
where: {
|
||||
id: orderId,
|
||||
userId: session.user.id
|
||||
},
|
||||
include: {
|
||||
orderItems: {
|
||||
include: {
|
||||
product: {
|
||||
select: {
|
||||
name: true,
|
||||
images: true,
|
||||
discount: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true
|
||||
}
|
||||
},
|
||||
shippingAddress: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
address1: true,
|
||||
address2: true,
|
||||
city: true,
|
||||
state: true,
|
||||
zipCode: true,
|
||||
phone: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
return NextResponse.json({ error: 'Order not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Calculate estimated delivery (5-7 business days)
|
||||
const estimatedDelivery = new Date()
|
||||
estimatedDelivery.setDate(estimatedDelivery.getDate() + 7)
|
||||
|
||||
const response = {
|
||||
orderId: order.id,
|
||||
total: order.total,
|
||||
status: order.status,
|
||||
createdAt: order.createdAt,
|
||||
estimatedDelivery: estimatedDelivery.toISOString(),
|
||||
customer: {
|
||||
name: order.user.name || 'Customer',
|
||||
email: order.user.email
|
||||
},
|
||||
shippingAddress: order.shippingAddress ? {
|
||||
name: `${order.shippingAddress.firstName} ${order.shippingAddress.lastName}`,
|
||||
address: order.shippingAddress.address1,
|
||||
address2: order.shippingAddress.address2,
|
||||
city: order.shippingAddress.city,
|
||||
state: order.shippingAddress.state,
|
||||
zipCode: order.shippingAddress.zipCode,
|
||||
phone: order.shippingAddress.phone
|
||||
} : null,
|
||||
items: order.orderItems.map(item => ({
|
||||
id: item.id,
|
||||
name: item.product.name,
|
||||
quantity: item.quantity,
|
||||
price: item.price,
|
||||
image: item.product.images[0] || null,
|
||||
discount: item.product.discount || 0
|
||||
}))
|
||||
}
|
||||
|
||||
return NextResponse.json(response)
|
||||
} catch (error) {
|
||||
console.error('Error fetching order confirmation:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch order details' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
63
app/api/orders/[id]/confirm/route.ts
Normal file
63
app/api/orders/[id]/confirm/route.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { calculateCommissions } from '@/lib/commission'
|
||||
|
||||
export async function POST(request: NextRequest, props: { params: Promise<{ id: string }> }) {
|
||||
const params = await props.params;
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const orderId = params.id
|
||||
|
||||
// Verify order belongs to user
|
||||
const order = await prisma.order.findFirst({
|
||||
where: {
|
||||
id: orderId,
|
||||
userId: session.user.id
|
||||
}
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
return NextResponse.json({ error: 'Order not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (order.status !== 'PENDING') {
|
||||
return NextResponse.json({ error: 'Order already processed' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Update order status and calculate commissions in transaction
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
// Update order status
|
||||
const updatedOrder = await tx.order.update({
|
||||
where: { id: orderId },
|
||||
data: { status: 'PAID' }
|
||||
})
|
||||
|
||||
// Calculate and create commissions
|
||||
const commissionResult = await calculateCommissions(orderId)
|
||||
|
||||
return {
|
||||
order: updatedOrder,
|
||||
commissions: commissionResult
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
order: result.order,
|
||||
commissions: result.commissions
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error confirming order:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to confirm order' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
110
app/api/orders/[id]/route.ts
Normal file
110
app/api/orders/[id]/route.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
const order = await prisma.order.findFirst({
|
||||
where: {
|
||||
id,
|
||||
userId: session.user.id
|
||||
},
|
||||
include: {
|
||||
orderItems: {
|
||||
include: {
|
||||
product: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
images: true,
|
||||
discount: true,
|
||||
sku: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
return NextResponse.json({ error: 'Order not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json(order)
|
||||
} catch (error) {
|
||||
console.error('Error fetching order:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch order' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
const { status } = await request.json()
|
||||
|
||||
// Only allow cancellation of pending orders
|
||||
if (status === 'CANCELLED') {
|
||||
const order = await prisma.order.findFirst({
|
||||
where: {
|
||||
id,
|
||||
userId: session.user.id,
|
||||
status: 'PENDING'
|
||||
}
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Order not found or cannot be cancelled' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const updatedOrder = await prisma.order.update({
|
||||
where: { id },
|
||||
data: { status: 'CANCELLED' }
|
||||
})
|
||||
|
||||
return NextResponse.json(updatedOrder)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid status update' },
|
||||
{ status: 400 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error updating order:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update order' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
90
app/api/orders/cancel/route.ts
Normal file
90
app/api/orders/cancel/route.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { orderId, reason } = await request.json()
|
||||
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
// Get order with items
|
||||
const order = await tx.order.findFirst({
|
||||
where: {
|
||||
id: orderId,
|
||||
userId: session.user.id,
|
||||
status: { in: ['PENDING', 'PAID'] } // Only allow cancellation for these statuses
|
||||
},
|
||||
include: {
|
||||
orderItems: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
throw new Error('Order not found or cannot be cancelled')
|
||||
}
|
||||
|
||||
// Update order status
|
||||
const updatedOrder = await tx.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
status: 'CANCELLED'
|
||||
}
|
||||
})
|
||||
|
||||
// Restore stock for all items
|
||||
for (const item of order.orderItems) {
|
||||
await tx.product.update({
|
||||
where: { id: item.productId },
|
||||
data: {
|
||||
stock: {
|
||||
increment: item.quantity
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// If order was paid, reverse commissions
|
||||
if (order.status === 'PAID') {
|
||||
await tx.commission.updateMany({
|
||||
where: { orderId: order.id },
|
||||
data: { status: 'CANCELLED' }
|
||||
})
|
||||
|
||||
// Reverse wallet earnings (you may want to handle this differently)
|
||||
const commissions = await tx.commission.findMany({
|
||||
where: { orderId: order.id }
|
||||
})
|
||||
|
||||
for (const commission of commissions) {
|
||||
await tx.wallet.update({
|
||||
where: { userId: commission.userId },
|
||||
data: {
|
||||
balance: { decrement: commission.amount },
|
||||
totalEarnings: { decrement: commission.amount }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return updatedOrder
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
order: result
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error cancelling order:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to cancel order' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
186
app/api/orders/route.ts
Normal file
186
app/api/orders/route.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { createRazorpayInstance, getRazorpayKeyId } from '@/lib/razorpay'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Authentication required to place orders' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await request.json()
|
||||
const { items, total, shippingAddress, shippingAddressId } = data
|
||||
|
||||
// Validate required fields
|
||||
if (!items || !Array.isArray(items) || items.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Order items are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!total || total <= 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Valid order total is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate stock availability for all items
|
||||
for (const item of items) {
|
||||
const product = await prisma.product.findUnique({
|
||||
where: { id: item.productId },
|
||||
select: { stock: true, name: true, isActive: true }
|
||||
})
|
||||
|
||||
if (!product) {
|
||||
return NextResponse.json(
|
||||
{ error: `Product not found: ${item.productId}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!product.isActive) {
|
||||
return NextResponse.json(
|
||||
{ error: `Product is no longer available: ${product.name}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (product.stock < item.quantity) {
|
||||
return NextResponse.json(
|
||||
{ error: `Insufficient stock for ${product.name}. Available: ${product.stock}, Requested: ${item.quantity}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const razorpay = createRazorpayInstance()
|
||||
const razorpayKeyId = getRazorpayKeyId()
|
||||
|
||||
console.log('Creating Razorpay order for amount:', total)
|
||||
console.log('Using Razorpay environment:', process.env.RAZORPAY_ENV || 'test')
|
||||
console.log('Razorpay Key ID:', razorpayKeyId)
|
||||
|
||||
// Create Razorpay order
|
||||
const razorpayOrder = await razorpay.orders.create({
|
||||
amount: Math.round(total * 100), // Convert to paise
|
||||
currency: 'INR',
|
||||
receipt: `order_${Date.now()}`,
|
||||
notes: {
|
||||
userId: session.user.id,
|
||||
itemCount: items.length,
|
||||
environment: process.env.RAZORPAY_ENV || 'test'
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Razorpay order created:', razorpayOrder.id)
|
||||
|
||||
// Use transaction to ensure atomicity
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
// Create order
|
||||
const order = await tx.order.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
total,
|
||||
status: 'PENDING',
|
||||
razorpayOrderId: razorpayOrder.id,
|
||||
shippingAddressId: shippingAddressId || null,
|
||||
orderItems: {
|
||||
create: items.map((item: any) => ({
|
||||
productId: item.productId,
|
||||
quantity: item.quantity,
|
||||
price: item.price
|
||||
}))
|
||||
}
|
||||
},
|
||||
include: {
|
||||
orderItems: {
|
||||
include: {
|
||||
product: true
|
||||
}
|
||||
},
|
||||
shippingAddress: true
|
||||
}
|
||||
})
|
||||
|
||||
// Reserve stock (reduce stock for pending orders)
|
||||
for (const item of items) {
|
||||
await tx.product.update({
|
||||
where: { id: item.productId },
|
||||
data: {
|
||||
stock: {
|
||||
decrement: item.quantity
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return order
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
id: result.id,
|
||||
total: result.total,
|
||||
status: result.status,
|
||||
createdAt: result.createdAt,
|
||||
razorpayOrderId: razorpayOrder.id,
|
||||
razorpayKeyId: razorpayKeyId,
|
||||
isProduction: process.env.RAZORPAY_ENV === 'production'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating order:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create order' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const orders = await prisma.order.findMany({
|
||||
where: {
|
||||
userId: session.user.id
|
||||
},
|
||||
include: {
|
||||
orderItems: {
|
||||
include: {
|
||||
product: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
images: true,
|
||||
discount: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json(orders)
|
||||
} catch (error) {
|
||||
console.error('Error fetching orders:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch orders' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
218
app/api/orders/verify-payment/route.ts
Normal file
218
app/api/orders/verify-payment/route.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getRazorpayConfig } from '@/lib/razorpay'
|
||||
import crypto from 'crypto'
|
||||
import { emailService } from '@/lib/email'
|
||||
import { rankSystem } from '@/lib/ranks'
|
||||
import { calculateCommissions } from '@/lib/commission'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { razorpay_order_id, razorpay_payment_id, razorpay_signature, orderId } = await request.json()
|
||||
|
||||
const config = getRazorpayConfig()
|
||||
|
||||
// Verify signature
|
||||
const hmac = crypto.createHmac('sha256', config.keySecret)
|
||||
hmac.update(razorpay_order_id + '|' + razorpay_payment_id)
|
||||
const generated_signature = hmac.digest('hex')
|
||||
|
||||
if (generated_signature !== razorpay_signature) {
|
||||
console.error('Payment signature verification failed')
|
||||
console.error('Generated:', generated_signature)
|
||||
console.error('Received:', razorpay_signature)
|
||||
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Process successful payment in transaction
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
// Update order status
|
||||
const order = await tx.order.update({
|
||||
where: {
|
||||
id: orderId,
|
||||
userId: session.user.id
|
||||
},
|
||||
data: {
|
||||
status: 'PAID',
|
||||
razorpayPaymentId: razorpay_payment_id
|
||||
},
|
||||
include: {
|
||||
orderItems: {
|
||||
include: {
|
||||
product: true
|
||||
}
|
||||
},
|
||||
user: {
|
||||
include: {
|
||||
referrer: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return order
|
||||
})
|
||||
|
||||
// Calculate commissions after successful payment
|
||||
if (result.user.referrerId) {
|
||||
const commissionResult = await calculateCommissions(result.id)
|
||||
console.log('Commission calculation result:', commissionResult)
|
||||
}
|
||||
|
||||
// Send order confirmation email (don't block on failure)
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
const emailResult = await emailService.sendOrderConfirmation(
|
||||
result.user.email,
|
||||
result.user.name || 'Customer',
|
||||
result
|
||||
)
|
||||
if (!emailResult.skipped) {
|
||||
console.log('Order confirmation email sent successfully')
|
||||
}
|
||||
} catch (emailError) {
|
||||
console.error('Failed to send order confirmation email:', emailError)
|
||||
}
|
||||
})
|
||||
|
||||
// Update ranks for user and referrers (don't block on failure)
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await rankSystem.updateUserRank(result.userId)
|
||||
if (result.user.referrerId) {
|
||||
await rankSystem.updateUserRank(result.user.referrerId)
|
||||
}
|
||||
console.log('User ranks updated successfully')
|
||||
} catch (rankError) {
|
||||
console.error('Failed to update ranks:', rankError)
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Payment verified and order processed successfully')
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
order: result
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error verifying payment:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to verify payment' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
async function generateCommissions(tx: any, order: any) {
|
||||
// Check if commission settings exist, if not create default ones
|
||||
const commissionSettings = await tx.commissionSettings.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: { level: 'asc' }
|
||||
})
|
||||
|
||||
// If no settings exist, create default commission structure
|
||||
if (commissionSettings.length === 0) {
|
||||
await tx.commissionSettings.createMany({
|
||||
data: [
|
||||
{ level: 1, percentage: 10, isActive: true },
|
||||
{ level: 2, percentage: 5, isActive: true },
|
||||
{ level: 3, percentage: 3, isActive: true }
|
||||
]
|
||||
})
|
||||
|
||||
// Refetch settings
|
||||
const newSettings = await tx.commissionSettings.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: { level: 'asc' }
|
||||
})
|
||||
|
||||
await processCommissions(tx, order, newSettings)
|
||||
} else {
|
||||
await processCommissions(tx, order, commissionSettings)
|
||||
}
|
||||
}
|
||||
|
||||
async function processCommissions(tx: any, order: any, settings: any[]) {
|
||||
let currentUser = order.user
|
||||
let level = 1
|
||||
|
||||
for (const setting of settings) {
|
||||
if (!currentUser.referrerId || level > setting.level) break
|
||||
|
||||
const referrer = await tx.user.findUnique({
|
||||
where: { id: currentUser.referrerId },
|
||||
include: { referrer: true, currentRank: true }
|
||||
})
|
||||
|
||||
if (!referrer) break
|
||||
|
||||
// Apply rank multiplier to commission
|
||||
let commissionAmount = (order.total * setting.percentage) / 100
|
||||
if (referrer.currentRank) {
|
||||
commissionAmount *= referrer.currentRank.commissionMultiplier
|
||||
}
|
||||
|
||||
// Create commission record
|
||||
const commission = await tx.commission.create({
|
||||
data: {
|
||||
userId: referrer.id,
|
||||
fromUserId: order.userId,
|
||||
orderId: order.id,
|
||||
amount: commissionAmount,
|
||||
level: setting.level,
|
||||
type: 'REFERRAL',
|
||||
status: 'APPROVED'
|
||||
},
|
||||
include: {
|
||||
fromUser: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Update referrer's wallet
|
||||
await tx.wallet.upsert({
|
||||
where: { userId: referrer.id },
|
||||
update: {
|
||||
balance: { increment: commissionAmount },
|
||||
totalEarnings: { increment: commissionAmount }
|
||||
},
|
||||
create: {
|
||||
userId: referrer.id,
|
||||
balance: commissionAmount,
|
||||
totalEarnings: commissionAmount,
|
||||
totalWithdrawn: 0
|
||||
}
|
||||
})
|
||||
|
||||
// Send commission email (async, don't block transaction)
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
const emailResult = await emailService.sendCommissionAlert(
|
||||
referrer.email,
|
||||
referrer.name || 'User',
|
||||
commission
|
||||
)
|
||||
if (!emailResult.skipped) {
|
||||
console.log('Commission alert email sent successfully')
|
||||
}
|
||||
} catch (emailError) {
|
||||
console.error('Failed to send commission email:', emailError)
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`Generated commission of ₹${commissionAmount.toFixed(2)} for user ${referrer.id} at level ${setting.level}`)
|
||||
|
||||
currentUser = referrer
|
||||
level++
|
||||
}
|
||||
}
|
||||
160
app/api/part-time/register/route.ts
Normal file
160
app/api/part-time/register/route.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { EmailService } from '@/lib/email'
|
||||
|
||||
interface PartTimeRegistrationData {
|
||||
// Personal Information
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
phone: string
|
||||
age: string
|
||||
gender: string
|
||||
|
||||
// Address Information
|
||||
address: string
|
||||
city: string
|
||||
state: string
|
||||
zipCode: string
|
||||
|
||||
// Professional Information
|
||||
education: string
|
||||
experience: string
|
||||
preferredRole: string
|
||||
availableHours: string
|
||||
availableDays: string
|
||||
|
||||
// Additional Information
|
||||
skills: string
|
||||
motivation: string
|
||||
previousWorkExperience: string
|
||||
languagesKnown: string
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const data: PartTimeRegistrationData = await request.json()
|
||||
// Validate required fields
|
||||
const requiredFields = [
|
||||
'firstName', 'lastName', 'email', 'phone', 'age', 'gender',
|
||||
'address', 'city', 'state', 'zipCode',
|
||||
'education', 'experience', 'preferredRole', 'availableHours', 'availableDays',
|
||||
'motivation'
|
||||
]
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (!data[field as keyof PartTimeRegistrationData]) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: `${field} is required` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: data.email }
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Email already registered' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate a temporary password
|
||||
const tempPassword = Math.random().toString(36).slice(-8)
|
||||
const hashedPassword = await bcrypt.hash(tempPassword, 10)
|
||||
|
||||
// Create user with PART_TIME role
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
name: `${data.firstName} ${data.lastName}`,
|
||||
email: data.email,
|
||||
password: hashedPassword,
|
||||
role: 'PART_TIME',
|
||||
phone: data.phone,
|
||||
address: `${data.address}, ${data.city}, ${data.state} ${data.zipCode}`,
|
||||
isActive: true,
|
||||
joinedAt: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
// Create address record
|
||||
await prisma.address.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
address1: data.address,
|
||||
city: data.city,
|
||||
state: data.state,
|
||||
zipCode: data.zipCode,
|
||||
country: 'India',
|
||||
phone: data.phone,
|
||||
isDefault: true,
|
||||
type: 'HOME'
|
||||
}
|
||||
})
|
||||
|
||||
// Create form response for admin tracking
|
||||
await prisma.formResponse.create({
|
||||
data: {
|
||||
formId: 'part_time_registration',
|
||||
data: {
|
||||
...data,
|
||||
userId: user.id,
|
||||
registrationDate: new Date().toISOString()
|
||||
},
|
||||
status: 'new',
|
||||
userId: user.id
|
||||
}
|
||||
})
|
||||
|
||||
// Send email notifications
|
||||
const emailService = new EmailService()
|
||||
|
||||
// Send welcome email to part-time applicant
|
||||
await emailService.sendPartTimeWelcomeEmail({
|
||||
to: user.email,
|
||||
name: user.name || 'Applicant',
|
||||
email: user.email,
|
||||
password: tempPassword,
|
||||
preferredRole: data.preferredRole
|
||||
})
|
||||
|
||||
// Send admin notification
|
||||
await emailService.sendPartTimeAdminNotification({
|
||||
applicantName: user.name || 'Unknown',
|
||||
applicantEmail: user.email,
|
||||
preferredRole: data.preferredRole,
|
||||
phone: data.phone,
|
||||
availableHours: data.availableHours,
|
||||
availableDays: data.availableDays,
|
||||
motivation: data.motivation,
|
||||
registrationDate: new Date().toLocaleDateString()
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Part-time job application submitted successfully',
|
||||
loginCredentials: {
|
||||
email: user.email,
|
||||
password: tempPassword
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Part-time registration error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'Registration failed',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
222
app/api/partnership/apply/route.ts
Normal file
222
app/api/partnership/apply/route.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { EmailService } from '@/lib/email'
|
||||
|
||||
const emailService = new EmailService()
|
||||
|
||||
interface PartnerApplicationData {
|
||||
// Personal Information
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
phone: string
|
||||
|
||||
// Business Information
|
||||
businessName?: string
|
||||
businessType: string
|
||||
experience: string
|
||||
|
||||
// Partnership Details
|
||||
partnershipTier: 'Diamond' | 'Gold' | 'Silver'
|
||||
expectedCustomers: number
|
||||
|
||||
// Address Information
|
||||
address: string
|
||||
city: string
|
||||
state: string
|
||||
zipCode: string
|
||||
|
||||
// Additional Information
|
||||
motivation: string
|
||||
marketingPlan: string
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const data: PartnerApplicationData = await request.json()
|
||||
|
||||
// Validate required fields
|
||||
if (!data.firstName || !data.lastName || !data.email || !data.phone || !data.partnershipTier) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Missing required fields' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: data.email }
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'User with this email already exists' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check tier availability
|
||||
const tierAvailability = await checkTierAvailability(data.partnershipTier)
|
||||
if (!tierAvailability.available) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: `${data.partnershipTier} tier is currently full. Please select another tier.`,
|
||||
availableTiers: tierAvailability.availableTiers
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate random password and referral code
|
||||
const randomPassword = Math.random().toString(36).slice(-8) + Math.random().toString(36).slice(-8)
|
||||
const hashedPassword = await bcrypt.hash(randomPassword, 12)
|
||||
const referralCode = generateReferralCode(data.firstName, data.lastName)
|
||||
|
||||
// Create user with MEMBER role
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
name: `${data.firstName} ${data.lastName}`,
|
||||
email: data.email,
|
||||
phone: data.phone,
|
||||
password: hashedPassword,
|
||||
role: 'MEMBER',
|
||||
address: `${data.address}, ${data.city}, ${data.state} ${data.zipCode}`,
|
||||
isActive: true,
|
||||
partnerTier: data.partnershipTier,
|
||||
minReferrals: 3, // Partners need to add minimum 3 members
|
||||
referralCode: referralCode,
|
||||
}
|
||||
})
|
||||
|
||||
// Create address record
|
||||
await prisma.address.create({
|
||||
data: {
|
||||
userId: newUser.id,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
company: data.businessName || '',
|
||||
address1: data.address,
|
||||
city: data.city,
|
||||
state: data.state,
|
||||
zipCode: data.zipCode,
|
||||
phone: data.phone,
|
||||
isDefault: true,
|
||||
type: 'HOME'
|
||||
}
|
||||
})
|
||||
|
||||
// Store form response for admin review
|
||||
await prisma.formResponse.create({
|
||||
data: {
|
||||
formId: 'partnership_application',
|
||||
userId: newUser.id,
|
||||
data: {
|
||||
...data,
|
||||
applicationStatus: 'approved', // Auto-approve for now
|
||||
partnerTier: data.partnershipTier
|
||||
},
|
||||
status: 'approved'
|
||||
}
|
||||
})
|
||||
|
||||
// Create wallet for the partner
|
||||
await prisma.wallet.create({
|
||||
data: {
|
||||
userId: newUser.id,
|
||||
balance: 0,
|
||||
totalEarnings: 0,
|
||||
totalWithdrawn: 0
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
// Send admin notification email
|
||||
await emailService.sendPartnerApplicationAdminNotification(data, newUser)
|
||||
|
||||
// Send confirmation email to partner
|
||||
await emailService.sendPartnerWelcomeEmail(data, newUser, randomPassword)
|
||||
} catch (emailError) {
|
||||
console.error('Email sending failed:', emailError)
|
||||
// Don't fail the request if email fails
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Partnership application submitted successfully! Check your email for login details.',
|
||||
user: {
|
||||
id: newUser.id,
|
||||
email: newUser.email,
|
||||
name: newUser.name,
|
||||
role: newUser.role,
|
||||
referralCode: newUser.referralCode
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Partnership application error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate referral code
|
||||
function generateReferralCode(firstName: string, lastName: string): string {
|
||||
const initials = (firstName.charAt(0) + lastName.charAt(0)).toUpperCase()
|
||||
const timestamp = Date.now().toString().slice(-6)
|
||||
return `${initials}${timestamp}`
|
||||
}
|
||||
|
||||
// Check tier availability
|
||||
async function checkTierAvailability(requestedTier: string) {
|
||||
// Define tier limits
|
||||
const tierLimits = {
|
||||
Diamond: 100, // Maximum 100 Diamond partners
|
||||
Gold: 500, // Maximum 500 Gold partners
|
||||
Silver: 1000 // Maximum 1000 Silver partners
|
||||
}
|
||||
|
||||
// Count current partners for each tier
|
||||
const tierCounts = await prisma.user.groupBy({
|
||||
by: ['partnerTier'],
|
||||
where: {
|
||||
role: 'MEMBER',
|
||||
partnerTier: {
|
||||
in: ['Diamond', 'Gold', 'Silver']
|
||||
}
|
||||
},
|
||||
_count: {
|
||||
_all: true
|
||||
}
|
||||
})
|
||||
|
||||
// Create a map of current counts
|
||||
const currentCounts: Record<string, number> = {}
|
||||
tierCounts.forEach(tier => {
|
||||
if (tier.partnerTier) {
|
||||
currentCounts[tier.partnerTier] = tier._count._all
|
||||
}
|
||||
})
|
||||
|
||||
// Check availability for all tiers
|
||||
const availability = {
|
||||
Diamond: (currentCounts.Diamond || 0) < tierLimits.Diamond,
|
||||
Gold: (currentCounts.Gold || 0) < tierLimits.Gold,
|
||||
Silver: (currentCounts.Silver || 0) < tierLimits.Silver
|
||||
}
|
||||
|
||||
// Get available tiers
|
||||
const availableTiers = Object.entries(availability)
|
||||
.filter(([_, available]) => available)
|
||||
.map(([tier, _]) => tier)
|
||||
|
||||
return {
|
||||
available: availability[requestedTier as keyof typeof availability] || false,
|
||||
availableTiers,
|
||||
currentCounts,
|
||||
limits: tierLimits
|
||||
}
|
||||
}
|
||||
188
app/api/partnership/members/route.ts
Normal file
188
app/api/partnership/members/route.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { EmailService } from '@/lib/email'
|
||||
|
||||
const emailService = new EmailService()
|
||||
|
||||
interface AddMemberData {
|
||||
name: string
|
||||
email: string
|
||||
phone: string
|
||||
address?: string
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if user is a partner (MEMBER role)
|
||||
const partner = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
include: {
|
||||
referrals: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!partner || partner.role !== 'MEMBER') {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Only partners can add members' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if partner has minimum required members
|
||||
if (partner.referrals.length < partner.minReferrals) {
|
||||
// Partner is still building their minimum required members
|
||||
console.log(`Partner needs ${partner.minReferrals - partner.referrals.length} more members to meet minimum requirement`)
|
||||
}
|
||||
|
||||
const data: AddMemberData = await request.json()
|
||||
|
||||
// Validate required fields
|
||||
if (!data.name || !data.email || !data.phone) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Name, email, and phone are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: data.email }
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'User with this email already exists' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate random password
|
||||
const randomPassword = Math.random().toString(36).slice(-8) + Math.random().toString(36).slice(-8)
|
||||
const hashedPassword = await bcrypt.hash(randomPassword, 12)
|
||||
|
||||
// Create new member user
|
||||
const newMember = await prisma.user.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
phone: data.phone,
|
||||
password: hashedPassword,
|
||||
role: 'CUSTOMER',
|
||||
referrerId: partner.id,
|
||||
address: data.address,
|
||||
isActive: true,
|
||||
}
|
||||
})
|
||||
|
||||
// Create wallet for the new member
|
||||
await prisma.wallet.create({
|
||||
data: {
|
||||
userId: newMember.id,
|
||||
balance: 0,
|
||||
totalEarnings: 0,
|
||||
totalWithdrawn: 0
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
// Send notification to admin
|
||||
await emailService.sendMemberAddedAdminNotification(partner, newMember)
|
||||
|
||||
// Send welcome email to new member
|
||||
await emailService.sendMemberWelcomeEmail(data, partner, randomPassword)
|
||||
} catch (emailError) {
|
||||
console.error('Email sending failed:', emailError)
|
||||
// Don't fail the request if email fails
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Member added successfully! They will receive login details via email.',
|
||||
member: {
|
||||
id: newMember.id,
|
||||
name: newMember.name,
|
||||
email: newMember.email,
|
||||
phone: newMember.phone,
|
||||
referralCode: newMember.referralCode
|
||||
},
|
||||
totalMembers: partner.referrals.length + 1,
|
||||
minRequired: partner.minReferrals,
|
||||
isMinimumMet: (partner.referrals.length + 1) >= partner.minReferrals
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Add member error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Get partner's members
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get partner with their referrals
|
||||
const partner = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
include: {
|
||||
referrals: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
phone: true,
|
||||
joinedAt: true,
|
||||
isActive: true,
|
||||
referralCode: true
|
||||
},
|
||||
orderBy: {
|
||||
joinedAt: 'desc'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!partner || partner.role !== 'MEMBER') {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Only partners can view members' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
members: partner.referrals,
|
||||
totalMembers: partner.referrals.length,
|
||||
minRequired: partner.minReferrals,
|
||||
isMinimumMet: partner.referrals.length >= partner.minReferrals
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Get members error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
62
app/api/partnership/tiers/route.ts
Normal file
62
app/api/partnership/tiers/route.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Define tier limits
|
||||
const tierLimits = {
|
||||
Diamond: 500, // Maximum 500 Diamond partners
|
||||
Gold: 1500, // Maximum 1500 Gold partners
|
||||
Silver: 3000 // Maximum 3000 Silver partners
|
||||
}
|
||||
|
||||
// Count current partners for each tier
|
||||
const tierCounts = await prisma.user.groupBy({
|
||||
by: ['partnerTier'],
|
||||
where: {
|
||||
role: 'MEMBER',
|
||||
partnerTier: {
|
||||
in: ['Diamond', 'Gold', 'Silver']
|
||||
}
|
||||
},
|
||||
_count: {
|
||||
_all: true
|
||||
}
|
||||
})
|
||||
|
||||
// Create a map of current counts
|
||||
const currentCounts: Record<string, number> = {}
|
||||
tierCounts.forEach(tier => {
|
||||
if (tier.partnerTier) {
|
||||
currentCounts[tier.partnerTier] = tier._count._all
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate availability and remaining slots for each tier
|
||||
const tiers = Object.entries(tierLimits).map(([tierName, limit]) => {
|
||||
const currentCount = currentCounts[tierName] || 0
|
||||
const remaining = limit - currentCount
|
||||
|
||||
return {
|
||||
name: tierName,
|
||||
limit,
|
||||
currentCount,
|
||||
remaining,
|
||||
available: remaining > 0,
|
||||
percentageFull: Math.round((currentCount / limit) * 100)
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
tiers
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Tier availability error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Failed to fetch tier availability' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
58
app/api/payments/create-order/route.ts
Normal file
58
app/api/payments/create-order/route.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { createOrder } from '@/lib/razorpay'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { orderId } = await request.json()
|
||||
|
||||
// Get order details
|
||||
const order = await prisma.order.findUnique({
|
||||
where: {
|
||||
id: orderId,
|
||||
userId: session.user.id,
|
||||
status: 'PENDING'
|
||||
},
|
||||
include: {
|
||||
orderItems: {
|
||||
include: {
|
||||
product: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
return NextResponse.json({ error: 'Order not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Create Razorpay order
|
||||
const razorpayOrder = await createOrder(order.total, order.id)
|
||||
|
||||
// Update order with Razorpay order ID
|
||||
await prisma.order.update({
|
||||
where: { id: order.id },
|
||||
data: { razorpayOrderId: razorpayOrder.id }
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
orderId: razorpayOrder.id,
|
||||
amount: razorpayOrder.amount,
|
||||
currency: razorpayOrder.currency,
|
||||
key: process.env.RAZORPAY_KEY_ID,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error creating payment order:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create payment order' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
70
app/api/payments/verify/route.ts
Normal file
70
app/api/payments/verify/route.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { verifyPayment } from '@/lib/razorpay'
|
||||
import { CommissionService } from '@/lib/commission'
|
||||
import { emailService } from '@/lib/email'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const {
|
||||
razorpay_order_id,
|
||||
razorpay_payment_id,
|
||||
razorpay_signature,
|
||||
orderId
|
||||
} = await request.json()
|
||||
|
||||
// Verify payment signature
|
||||
const isValid = verifyPayment(
|
||||
razorpay_order_id,
|
||||
razorpay_payment_id,
|
||||
razorpay_signature
|
||||
)
|
||||
|
||||
if (!isValid) {
|
||||
return NextResponse.json({ error: 'Invalid payment signature' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Update order status
|
||||
const order = await prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
status: 'PAID',
|
||||
razorpayPaymentId: razorpay_payment_id
|
||||
},
|
||||
include: {
|
||||
orderItems: {
|
||||
include: {
|
||||
product: true
|
||||
}
|
||||
},
|
||||
user: true
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate and distribute commissions
|
||||
await CommissionService.calculateCommissions(order.id)
|
||||
|
||||
// Send order confirmation email
|
||||
await emailService.sendOrderConfirmation(
|
||||
order.user.email,
|
||||
order.user.name || 'Customer',
|
||||
order
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true, order })
|
||||
} catch (error) {
|
||||
console.error('Error verifying payment:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to verify payment' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
86
app/api/payouts/route.ts
Normal file
86
app/api/payouts/route.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getSystemSettings } from '@/lib/settings'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const payouts = await prisma.payout.findMany({
|
||||
where: { userId: session.user.id },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
|
||||
return NextResponse.json(payouts)
|
||||
} catch (error) {
|
||||
console.error('Error fetching payouts:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch payouts' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { amount, bankDetails } = await request.json()
|
||||
const settings = await getSystemSettings()
|
||||
|
||||
if (amount < settings.minimumPayout) {
|
||||
return NextResponse.json(
|
||||
{ error: `Minimum payout amount is ₹${settings.minimumPayout}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const wallet = await prisma.wallet.findUnique({
|
||||
where: { userId: session.user.id }
|
||||
})
|
||||
|
||||
if (!wallet || wallet.balance < amount) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Insufficient balance' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const payout = await tx.payout.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
amount,
|
||||
bankDetails,
|
||||
status: 'PENDING'
|
||||
}
|
||||
})
|
||||
|
||||
await tx.wallet.update({
|
||||
where: { userId: session.user.id },
|
||||
data: {
|
||||
balance: { decrement: amount }
|
||||
}
|
||||
})
|
||||
|
||||
return payout
|
||||
})
|
||||
|
||||
return NextResponse.json(result)
|
||||
} catch (error) {
|
||||
console.error('Error creating payout request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create payout request' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
87
app/api/pricing/wholesale/route.ts
Normal file
87
app/api/pricing/wholesale/route.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { PricingService } from '@/lib/pricing'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { productId, quantity } = await request.json()
|
||||
|
||||
if (!productId || !quantity) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Product ID and quantity are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get user role
|
||||
const userRole = await PricingService.getUserRole(session.user.id)
|
||||
|
||||
// Calculate pricing
|
||||
const pricing = await PricingService.getEffectivePrice(
|
||||
productId,
|
||||
quantity,
|
||||
{
|
||||
userId: session.user.id,
|
||||
userRole: userRole || undefined,
|
||||
quantity
|
||||
}
|
||||
)
|
||||
|
||||
// Get wholesale discount info
|
||||
const wholesaleInfo = PricingService.getWholesaleDiscountInfo()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
pricing,
|
||||
wholesaleInfo,
|
||||
userRole,
|
||||
isWholesaler: userRole === 'WHOLESALER'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Pricing calculation error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to calculate pricing'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Check if user is wholesaler
|
||||
const isWholesaler = await PricingService.isWholesaler(session.user.id)
|
||||
const wholesaleInfo = PricingService.getWholesaleDiscountInfo()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
isWholesaler,
|
||||
wholesaleInfo
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Wholesale check error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to check wholesale status'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
37
app/api/products/[slug]/route.ts
Normal file
37
app/api/products/[slug]/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(request: NextRequest, props: { params: Promise<{ slug: string }> }) {
|
||||
const params = await props.params;
|
||||
try {
|
||||
const product = await prisma.product.findUnique({
|
||||
where: {
|
||||
slug: params.slug,
|
||||
isActive: true
|
||||
},
|
||||
include: {
|
||||
category: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
if (!product) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Product not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(product)
|
||||
} catch (error) {
|
||||
console.error('Error fetching product by slug:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch product' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
214
app/api/products/route.ts
Normal file
214
app/api/products/route.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { productSchema } from '@/lib/validations/product'
|
||||
import { DatabaseOptimizer } from '@/lib/database-optimizer'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const category = searchParams.get('category')
|
||||
const search = searchParams.get('search')
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '12')
|
||||
const isAdmin = searchParams.get('admin') === 'true'
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
// Create cache key for this specific query
|
||||
const cacheKey = `products:${category || 'all'}:${search || 'none'}:${page}:${limit}:${isAdmin}`
|
||||
|
||||
// Try to get from cache first
|
||||
const cached = await DatabaseOptimizer.getCachedData(cacheKey)
|
||||
if (cached && !isAdmin) { // Don't cache admin requests
|
||||
return NextResponse.json({
|
||||
...cached,
|
||||
_performance: {
|
||||
responseTime: Date.now() - startTime,
|
||||
cached: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const where: any = {}
|
||||
|
||||
// Only filter by isActive if not admin request
|
||||
if (!isAdmin) {
|
||||
where.isActive = true
|
||||
// Add stock filter for better SEO (only show available products)
|
||||
where.stock = { gt: 0 }
|
||||
}
|
||||
|
||||
if (category) {
|
||||
where.categoryId = category
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ description: { contains: search, mode: 'insensitive' } },
|
||||
{
|
||||
category: {
|
||||
name: { contains: search, mode: 'insensitive' }
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// Optimize queries with parallel execution and selective field inclusion
|
||||
const [products, total, categoryStats] = await Promise.all([
|
||||
// Optimized products query with caching
|
||||
DatabaseOptimizer.executeOptimizedQuery(
|
||||
`products_list_${JSON.stringify(where)}_${skip}_${limit}`,
|
||||
() => prisma.product.findMany({
|
||||
where,
|
||||
include: {
|
||||
category: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
}
|
||||
},
|
||||
reviews: {
|
||||
select: {
|
||||
rating: true,
|
||||
createdAt: true,
|
||||
},
|
||||
take: 5, // Limit reviews for performance
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
reviews: true,
|
||||
orderItems: true, // For popularity metrics
|
||||
}
|
||||
}
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: [
|
||||
{ isActive: 'desc' }, // Active products first
|
||||
{ stock: 'desc' }, // In-stock products next
|
||||
{ createdAt: 'desc' }, // Then by creation date
|
||||
],
|
||||
}),
|
||||
300 // Cache for 5 minutes
|
||||
),
|
||||
// Optimized count query
|
||||
DatabaseOptimizer.executeOptimizedQuery(
|
||||
`products_count_${JSON.stringify(where)}`,
|
||||
() => prisma.product.count({ where }),
|
||||
600 // Cache for 10 minutes
|
||||
),
|
||||
// Optimized category stats
|
||||
DatabaseOptimizer.executeOptimizedQuery(
|
||||
`category_stats_${category || 'all'}`,
|
||||
() => prisma.product.groupBy({
|
||||
by: ['categoryId'],
|
||||
where: { isActive: true, stock: { gt: 0 } },
|
||||
_count: {
|
||||
categoryId: true,
|
||||
},
|
||||
orderBy: {
|
||||
_count: {
|
||||
categoryId: 'desc'
|
||||
}
|
||||
}
|
||||
}),
|
||||
900 // Cache for 15 minutes
|
||||
)
|
||||
])
|
||||
|
||||
|
||||
// Calculate advanced metrics for SEO and performance
|
||||
const productsWithMetrics = products.map(product => ({
|
||||
...product,
|
||||
averageRating: product.reviews.length > 0
|
||||
? Math.round((product.reviews.reduce((acc, review) => acc + review.rating, 0) / product.reviews.length) * 10) / 10
|
||||
: null,
|
||||
reviewCount: product._count.reviews,
|
||||
popularityScore: product._count.orderItems,
|
||||
hasRecentReviews: product.reviews.some(review =>
|
||||
new Date(review.createdAt) > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
||||
),
|
||||
isPopular: product._count.orderItems > 10,
|
||||
inStock: product.stock > 0,
|
||||
lowStock: product.stock > 0 && product.stock <= 10,
|
||||
// Remove internal data from response
|
||||
reviews: undefined,
|
||||
_count: undefined,
|
||||
}))
|
||||
|
||||
const responseData = {
|
||||
products: productsWithMetrics,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
seo: {
|
||||
totalProducts: total,
|
||||
categoryDistribution: categoryStats,
|
||||
hasProducts: total > 0
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the response for non-admin requests
|
||||
if (!isAdmin) {
|
||||
await DatabaseOptimizer.setCachedData(cacheKey, responseData, 300)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
...responseData,
|
||||
_performance: {
|
||||
responseTime: Date.now() - startTime,
|
||||
cached: false
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Products API error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = productSchema.parse(body)
|
||||
|
||||
// Generate SKU and slug if not provided
|
||||
const sku = validatedData.sku || `SKU-${Date.now()}`
|
||||
const slug = validatedData.slug || validatedData.name.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '')
|
||||
|
||||
const product = await prisma.product.create({
|
||||
data: {
|
||||
...validatedData,
|
||||
sku,
|
||||
slug
|
||||
},
|
||||
include: {
|
||||
category: true,
|
||||
},
|
||||
})
|
||||
|
||||
// Invalidate product caches after creation
|
||||
DatabaseOptimizer.invalidateCache('products')
|
||||
DatabaseOptimizer.invalidateCache('category_stats')
|
||||
|
||||
return NextResponse.json(product, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Create product error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
96
app/api/reviews/[id]/helpful/route.ts
Normal file
96
app/api/reviews/[id]/helpful/route.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: reviewId } = await params
|
||||
|
||||
// Check if review exists
|
||||
const review = await prisma.review.findUnique({
|
||||
where: { id: reviewId }
|
||||
})
|
||||
|
||||
if (!review) {
|
||||
return NextResponse.json({ error: 'Review not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check if user already voted
|
||||
const existingVote = await prisma.reviewHelpfulVote.findUnique({
|
||||
where: {
|
||||
reviewId_userId: {
|
||||
reviewId,
|
||||
userId: session.user.id
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (existingVote) {
|
||||
// Remove the vote (toggle)
|
||||
await prisma.reviewHelpfulVote.delete({
|
||||
where: { id: existingVote.id }
|
||||
})
|
||||
|
||||
// Update helpful count
|
||||
const updatedReview = await prisma.review.update({
|
||||
where: { id: reviewId },
|
||||
data: {
|
||||
helpfulVotes: {
|
||||
decrement: 1
|
||||
}
|
||||
},
|
||||
select: {
|
||||
helpfulVotes: true
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Helpful vote removed',
|
||||
hasVoted: false,
|
||||
totalVotes: updatedReview.helpfulVotes
|
||||
})
|
||||
} else {
|
||||
// Add the vote
|
||||
await prisma.reviewHelpfulVote.create({
|
||||
data: {
|
||||
reviewId,
|
||||
userId: session.user.id
|
||||
}
|
||||
})
|
||||
|
||||
// Update helpful count
|
||||
const updatedReview = await prisma.review.update({
|
||||
where: { id: reviewId },
|
||||
data: {
|
||||
helpfulVotes: {
|
||||
increment: 1
|
||||
}
|
||||
},
|
||||
select: {
|
||||
helpfulVotes: true
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Review marked as helpful',
|
||||
hasVoted: true,
|
||||
totalVotes: updatedReview.helpfulVotes
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error voting on review:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to vote on review' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
85
app/api/reviews/[id]/report/route.ts
Normal file
85
app/api/reviews/[id]/report/route.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: reviewId } = await params
|
||||
const body = await request.json()
|
||||
const { reason, comment } = body
|
||||
|
||||
// Validate reason
|
||||
const validReasons = ['SPAM', 'INAPPROPRIATE', 'FAKE', 'OFFENSIVE', 'OTHER']
|
||||
if (!reason || !validReasons.includes(reason)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Valid reason is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if review exists
|
||||
const review = await prisma.review.findUnique({
|
||||
where: { id: reviewId }
|
||||
})
|
||||
|
||||
if (!review) {
|
||||
return NextResponse.json({ error: 'Review not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check if user already reported this review
|
||||
const existingReport = await prisma.reviewReport.findUnique({
|
||||
where: {
|
||||
reviewId_userId: {
|
||||
reviewId,
|
||||
userId: session.user.id
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (existingReport) {
|
||||
return NextResponse.json(
|
||||
{ error: 'You have already reported this review' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Create the report
|
||||
await prisma.reviewReport.create({
|
||||
data: {
|
||||
reviewId,
|
||||
userId: session.user.id,
|
||||
reason,
|
||||
comment
|
||||
}
|
||||
})
|
||||
|
||||
// Update report count
|
||||
await prisma.review.update({
|
||||
where: { id: reviewId },
|
||||
data: {
|
||||
reportCount: {
|
||||
increment: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Review reported successfully. Thank you for helping us maintain quality.'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error reporting review:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to report review' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
164
app/api/reviews/[id]/route.ts
Normal file
164
app/api/reviews/[id]/route.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
const review = await prisma.review.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
}
|
||||
},
|
||||
product: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
}
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
helpfulVotedBy: true,
|
||||
reportedBy: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!review) {
|
||||
return NextResponse.json({ error: 'Review not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json(review)
|
||||
} catch (error) {
|
||||
console.error('Error fetching review:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch review' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const { rating, title, comment, images } = body
|
||||
|
||||
// Find the review and check ownership
|
||||
const existingReview = await prisma.review.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
if (!existingReview) {
|
||||
return NextResponse.json({ error: 'Review not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (existingReview.userId !== session.user.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Validate rating if provided
|
||||
if (rating && (rating < 1 || rating > 5)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Rating must be between 1 and 5' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const updatedReview = await prisma.review.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(rating && { rating }),
|
||||
...(title !== undefined && { title }),
|
||||
...(comment !== undefined && { comment }),
|
||||
...(images && { images }),
|
||||
isApproved: false, // Reset approval status when edited
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
}
|
||||
},
|
||||
product: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
review: updatedReview,
|
||||
message: 'Review updated successfully. It will be visible after admin approval.'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error updating review:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update review' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
// Find the review and check ownership or admin access
|
||||
const existingReview = await prisma.review.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
if (!existingReview) {
|
||||
return NextResponse.json({ error: 'Review not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (existingReview.userId !== session.user.id && session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
await prisma.review.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return NextResponse.json({ message: 'Review deleted successfully' })
|
||||
} catch (error) {
|
||||
console.error('Error deleting review:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete review' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
130
app/api/reviews/route.ts
Normal file
130
app/api/reviews/route.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
// GET /api/reviews - Get reviews with filters
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
const { searchParams } = new URL(request.url)
|
||||
const productId = searchParams.get('productId')
|
||||
const userId = searchParams.get('userId')
|
||||
const rating = searchParams.get('rating')
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '10')
|
||||
const skip = (page - 1) * limit
|
||||
const withStats = searchParams.get('withStats') === 'true'
|
||||
|
||||
const where: any = {
|
||||
isApproved: true, // Only show approved reviews publicly
|
||||
}
|
||||
|
||||
if (productId) where.productId = productId
|
||||
if (userId) where.userId = userId
|
||||
if (rating) where.rating = parseInt(rating)
|
||||
|
||||
const [reviews, total, aggregateStats] = await Promise.all([
|
||||
prisma.review.findMany({
|
||||
where,
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
// Add user credibility for social proof
|
||||
_count: {
|
||||
select: {
|
||||
reviews: true, // Total reviews by this user
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
product: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
}
|
||||
},
|
||||
helpfulVotedBy: session?.user?.id ? {
|
||||
where: {
|
||||
userId: session.user.id
|
||||
}
|
||||
} : false,
|
||||
_count: {
|
||||
select: {
|
||||
helpfulVotedBy: true,
|
||||
reportedBy: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: [
|
||||
{ helpfulVotedBy: { _count: 'desc' } }, // Most helpful first
|
||||
{ rating: 'desc' }, // Higher ratings next
|
||||
{ createdAt: 'desc' } // Then by recency
|
||||
],
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.review.count({ where }),
|
||||
// Get aggregate stats for social proof
|
||||
withStats ? prisma.review.groupBy({
|
||||
by: ['rating'],
|
||||
where: productId ? { productId, isApproved: true } : { isApproved: true },
|
||||
_count: {
|
||||
rating: true,
|
||||
},
|
||||
}) : null,
|
||||
])
|
||||
|
||||
// Calculate enhanced social proof metrics
|
||||
const enhancedReviews = reviews.map(review => ({
|
||||
...review,
|
||||
// Add social proof indicators
|
||||
isVerifiedReviewer: review.user._count.reviews >= 3, // Users with 3+ reviews are "verified"
|
||||
helpfulnessScore: review._count.helpfulVotedBy,
|
||||
// Add time-based freshness
|
||||
isFresh: new Date(review.createdAt) > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // Last 30 days
|
||||
}))
|
||||
|
||||
// Calculate rating distribution for social proof
|
||||
let ratingDistribution = null
|
||||
if (aggregateStats) {
|
||||
const totalReviews = aggregateStats.reduce((sum, stat) => sum + stat._count.rating, 0)
|
||||
ratingDistribution = {
|
||||
1: 0, 2: 0, 3: 0, 4: 0, 5: 0,
|
||||
...aggregateStats.reduce((acc, stat) => ({
|
||||
...acc,
|
||||
[stat.rating]: Math.round((stat._count.rating / totalReviews) * 100)
|
||||
}), {})
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
reviews: enhancedReviews,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
// Enhanced social proof data
|
||||
socialProof: {
|
||||
totalReviews: total,
|
||||
averageRating: enhancedReviews.length > 0
|
||||
? enhancedReviews.reduce((sum, review) => sum + review.rating, 0) / enhancedReviews.length
|
||||
: 0,
|
||||
ratingDistribution,
|
||||
verifiedReviewersCount: enhancedReviews.filter(r => r.isVerifiedReviewer).length,
|
||||
recentReviewsCount: enhancedReviews.filter(r => r.isFresh).length,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching reviews:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch reviews' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
240
app/api/social-proof/route.ts
Normal file
240
app/api/social-proof/route.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { DatabaseOptimizer } from '@/lib/database-optimizer'
|
||||
|
||||
// GET /api/social-proof - Get aggregated social proof data
|
||||
export async function GET(request: NextRequest) {
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const productId = searchParams.get('productId')
|
||||
|
||||
// Create cache key
|
||||
const cacheKey = `social_proof_${productId || 'global'}`
|
||||
|
||||
// Try to get from cache first
|
||||
const cached = await DatabaseOptimizer.getCachedData(cacheKey)
|
||||
if (cached) {
|
||||
return NextResponse.json({
|
||||
...cached,
|
||||
_performance: {
|
||||
responseTime: Date.now() - startTime,
|
||||
cached: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Get comprehensive social proof metrics with optimization
|
||||
const [
|
||||
totalUsers,
|
||||
totalReviews,
|
||||
recentReviews,
|
||||
topRatedProducts,
|
||||
verifiedReviewers,
|
||||
recentOrders,
|
||||
averageRatings,
|
||||
] = await Promise.all([
|
||||
// Optimized total active users
|
||||
DatabaseOptimizer.executeOptimizedQuery(
|
||||
'total_active_users',
|
||||
() => prisma.user.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
role: 'CUSTOMER'
|
||||
}
|
||||
}),
|
||||
1800 // Cache for 30 minutes
|
||||
),
|
||||
|
||||
// Optimized total approved reviews
|
||||
DatabaseOptimizer.executeOptimizedQuery(
|
||||
`total_reviews_${productId || 'all'}`,
|
||||
() => prisma.review.count({
|
||||
where: {
|
||||
isApproved: true,
|
||||
...(productId && { productId })
|
||||
}
|
||||
}),
|
||||
600 // Cache for 10 minutes
|
||||
),
|
||||
|
||||
// Optimized recent reviews (last 7 days)
|
||||
DatabaseOptimizer.executeOptimizedQuery(
|
||||
`recent_reviews_${productId || 'all'}`,
|
||||
() => prisma.review.count({
|
||||
where: {
|
||||
isApproved: true,
|
||||
createdAt: {
|
||||
gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
||||
},
|
||||
...(productId && { productId })
|
||||
}
|
||||
}),
|
||||
300 // Cache for 5 minutes
|
||||
),
|
||||
|
||||
// Optimized top rated products for social proof
|
||||
DatabaseOptimizer.executeOptimizedQuery(
|
||||
'top_rated_products',
|
||||
() => prisma.product.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
stock: { gt: 0 },
|
||||
reviews: {
|
||||
some: {
|
||||
isApproved: true,
|
||||
rating: { gte: 4 }
|
||||
}
|
||||
}
|
||||
},
|
||||
include: {
|
||||
reviews: {
|
||||
where: { isApproved: true },
|
||||
select: { rating: true }
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
reviews: true,
|
||||
orderItems: true
|
||||
}
|
||||
}
|
||||
},
|
||||
take: 5,
|
||||
orderBy: {
|
||||
reviews: {
|
||||
_count: 'desc'
|
||||
}
|
||||
}
|
||||
}),
|
||||
900 // Cache for 15 minutes
|
||||
),
|
||||
|
||||
// Optimized verified reviewers - get users with multiple reviews
|
||||
DatabaseOptimizer.executeOptimizedQuery(
|
||||
'verified_reviewers_count',
|
||||
async () => {
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
reviews: {
|
||||
some: {
|
||||
isApproved: true
|
||||
}
|
||||
}
|
||||
},
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
reviews: {
|
||||
where: {
|
||||
isApproved: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return users.filter(user => user._count.reviews >= 3).length
|
||||
},
|
||||
1800 // Cache for 30 minutes
|
||||
),
|
||||
|
||||
// Optimized recent orders for activity proof
|
||||
DatabaseOptimizer.executeOptimizedQuery(
|
||||
'recent_orders_count',
|
||||
() => prisma.order.count({
|
||||
where: {
|
||||
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] },
|
||||
createdAt: {
|
||||
gte: new Date(Date.now() - 24 * 60 * 60 * 1000) // Last 24 hours
|
||||
}
|
||||
}
|
||||
}),
|
||||
300 // Cache for 5 minutes
|
||||
),
|
||||
|
||||
// Optimized average ratings by category
|
||||
DatabaseOptimizer.executeOptimizedQuery(
|
||||
`average_ratings_${productId || 'all'}`,
|
||||
() => prisma.review.groupBy({
|
||||
by: ['productId'],
|
||||
where: {
|
||||
isApproved: true,
|
||||
...(productId && { productId })
|
||||
},
|
||||
_avg: {
|
||||
rating: true
|
||||
},
|
||||
_count: {
|
||||
rating: true
|
||||
},
|
||||
having: {
|
||||
rating: {
|
||||
_count: {
|
||||
gte: 3 // At least 3 reviews
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
600 // Cache for 10 minutes
|
||||
)
|
||||
])
|
||||
|
||||
// Calculate enhanced social proof metrics
|
||||
const topRatedWithMetrics = topRatedProducts.map(product => ({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
averageRating: product.reviews.length > 0
|
||||
? Math.round((product.reviews.reduce((sum, review) => sum + review.rating, 0) / product.reviews.length) * 10) / 10
|
||||
: 0,
|
||||
totalReviews: product._count.reviews,
|
||||
totalOrders: product._count.orderItems,
|
||||
popularityScore: product._count.orderItems + (product._count.reviews * 2)
|
||||
})).filter(product => product.averageRating >= 4.0)
|
||||
|
||||
const overallAverageRating = averageRatings.length > 0
|
||||
? Math.round((averageRatings.reduce((sum, item) => sum + (item._avg.rating || 0), 0) / averageRatings.length) * 10) / 10
|
||||
: 0
|
||||
|
||||
const responseData = {
|
||||
socialProof: {
|
||||
totalCustomers: totalUsers,
|
||||
totalReviews,
|
||||
recentReviews,
|
||||
verifiedReviewers,
|
||||
recentOrders,
|
||||
overallAverageRating,
|
||||
topRatedProducts: topRatedWithMetrics,
|
||||
trustIndicators: {
|
||||
hasRecentActivity: recentOrders > 0,
|
||||
hasRecentReviews: recentReviews > 0,
|
||||
hasVerifiedReviewers: verifiedReviewers > 0,
|
||||
highRatingProducts: topRatedWithMetrics.length > 0,
|
||||
},
|
||||
metrics: {
|
||||
reviewEngagement: totalReviews > 0 ? Math.round((recentReviews / totalReviews) * 100) : 0,
|
||||
customerRetention: totalUsers > 0 ? Math.round((verifiedReviewers / totalUsers) * 100) : 0,
|
||||
productSatisfaction: Math.round(overallAverageRating * 20), // Convert to percentage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the response
|
||||
await DatabaseOptimizer.setCachedData(cacheKey, responseData, 600)
|
||||
|
||||
return NextResponse.json({
|
||||
...responseData,
|
||||
_performance: {
|
||||
responseTime: Date.now() - startTime,
|
||||
cached: false
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching social proof data:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch social proof data' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
85
app/api/team/route.ts
Normal file
85
app/api/team/route.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get direct referrals
|
||||
const directReferrals = await prisma.user.findMany({
|
||||
where: { referrerId: session.user.id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
role: true,
|
||||
joinedAt: true,
|
||||
referrals: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
role: true,
|
||||
joinedAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { joinedAt: 'desc' },
|
||||
})
|
||||
|
||||
// Get team statistics
|
||||
const teamStats = await getTeamStats(session.user.id)
|
||||
|
||||
return NextResponse.json({
|
||||
directReferrals,
|
||||
teamStats,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Team API error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
async function getTeamStats(userId: string) {
|
||||
const levels = []
|
||||
|
||||
for (let level = 1; level <= 5; level++) {
|
||||
const count = await getTeamCountAtLevel(userId, level)
|
||||
levels.push({ level, count })
|
||||
}
|
||||
|
||||
const totalTeamSize = levels.reduce((sum, level) => sum + level.count, 0)
|
||||
|
||||
return {
|
||||
totalTeamSize,
|
||||
levels,
|
||||
}
|
||||
}
|
||||
|
||||
async function getTeamCountAtLevel(userId: string, targetLevel: number, currentLevel = 0, visited = new Set<string>()): Promise<number> {
|
||||
if (currentLevel === targetLevel || visited.has(userId)) {
|
||||
return currentLevel === targetLevel ? 1 : 0
|
||||
}
|
||||
|
||||
visited.add(userId)
|
||||
|
||||
const directReferrals = await prisma.user.findMany({
|
||||
where: { referrerId: userId },
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
if (currentLevel === targetLevel - 1) {
|
||||
return directReferrals.length
|
||||
}
|
||||
|
||||
let count = 0
|
||||
for (const referral of directReferrals) {
|
||||
count += await getTeamCountAtLevel(referral.id, targetLevel, currentLevel + 1, visited)
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
194
app/api/upload/files/route.ts
Normal file
194
app/api/upload/files/route.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import blobStorage from '@/lib/blob-storage'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const files = formData.getAll('files') as File[]
|
||||
const folder = formData.get('folder') as string || 'general'
|
||||
const type = formData.get('type') as string || 'general'
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'No files provided' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate files
|
||||
for (const file of files) {
|
||||
if (!file || file.size === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Invalid file provided' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let uploadResults: any[] = []
|
||||
|
||||
try {
|
||||
// Handle different upload types
|
||||
switch (type) {
|
||||
case 'product-images': {
|
||||
const productName = formData.get('productName') as string
|
||||
if (!productName) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Product name is required for product images' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const urls = await blobStorage.uploadProductImages(files, productName)
|
||||
uploadResults = urls.map(url => ({ url, type: 'product-image' }))
|
||||
break
|
||||
}
|
||||
|
||||
case 'category-image': {
|
||||
if (files.length > 1) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Only one category image allowed' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const categoryName = formData.get('categoryName') as string
|
||||
if (!categoryName) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Category name is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const url = await blobStorage.uploadCategoryImage(files[0], categoryName)
|
||||
uploadResults = [{ url, type: 'category-image' }]
|
||||
break
|
||||
}
|
||||
|
||||
case 'user-avatar': {
|
||||
if (files.length > 1) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Only one avatar image allowed' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const userId = formData.get('userId') as string
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'User ID is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const url = await blobStorage.uploadUserAvatar(files[0], userId)
|
||||
uploadResults = [{ url, type: 'user-avatar' }]
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
// General file upload
|
||||
const results = await blobStorage.uploadMultipleFiles(files, { folder })
|
||||
uploadResults = results.map(result => ({
|
||||
url: result.url,
|
||||
downloadUrl: result.downloadUrl,
|
||||
pathname: result.pathname,
|
||||
size: result.size,
|
||||
uploadedAt: result.uploadedAt,
|
||||
type: 'general'
|
||||
}))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Files uploaded successfully',
|
||||
data: uploadResults
|
||||
})
|
||||
|
||||
} catch (uploadError) {
|
||||
console.error('Upload error:', uploadError)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: uploadError instanceof Error ? uploadError.message : 'Failed to upload files'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('File upload error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const url = searchParams.get('url')
|
||||
const urls = searchParams.getAll('urls')
|
||||
|
||||
if (!url && (!urls || urls.length === 0)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'No URL(s) provided for deletion' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (url) {
|
||||
// Delete single file
|
||||
await blobStorage.deleteFile(url)
|
||||
} else {
|
||||
// Delete multiple files
|
||||
await blobStorage.deleteMultipleFiles(urls)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'File(s) deleted successfully'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('File deletion error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Failed to delete file(s)'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const prefix = searchParams.get('prefix')
|
||||
const limit = parseInt(searchParams.get('limit') || '100')
|
||||
const cursor = searchParams.get('cursor')
|
||||
|
||||
const result = await blobStorage.listFiles({
|
||||
prefix: prefix || undefined,
|
||||
limit,
|
||||
cursor: cursor || undefined
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: result
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('File listing error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'Failed to list files'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
56
app/api/upload/route.ts
Normal file
56
app/api/upload/route.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { writeFile, mkdir } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { existsSync } from 'fs'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const data = await request.formData()
|
||||
const files: File[] = data.getAll('files') as File[]
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
return NextResponse.json({ error: 'No files uploaded' }, { status: 400 })
|
||||
}
|
||||
|
||||
const uploadedUrls: string[] = []
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return NextResponse.json({ error: 'Only image files are allowed' }, { status: 400 })
|
||||
}
|
||||
|
||||
const bytes = await file.arrayBuffer()
|
||||
const buffer = new Uint8Array(bytes)
|
||||
|
||||
// Create products directory if it doesn't exist
|
||||
const productsDir = join(process.cwd(), 'public', 'products')
|
||||
if (!existsSync(productsDir)) {
|
||||
await mkdir(productsDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Generate unique filename
|
||||
const timestamp = Date.now()
|
||||
const filename = `${timestamp}-${file.name.replace(/[^a-zA-Z0-9.-]/g, '_')}`
|
||||
const filepath = join(productsDir, filename)
|
||||
|
||||
// Write file to public/products directory
|
||||
await writeFile(filepath, buffer)
|
||||
|
||||
// Add to uploaded URLs (relative path for use in app)
|
||||
uploadedUrls.push(`/products/${filename}`)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
urls: uploadedUrls,
|
||||
message: `${uploadedUrls.length} file(s) uploaded successfully`
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to upload files' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
157
app/api/user/accounts/route.ts
Normal file
157
app/api/user/accounts/route.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const { provider, providerAccountId, access_token, refresh_token, expires_at } = await request.json()
|
||||
|
||||
if (!provider || !providerAccountId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if this account is already linked to another user
|
||||
const existingAccount = await prisma.account.findFirst({
|
||||
where: {
|
||||
provider,
|
||||
providerAccountId,
|
||||
},
|
||||
})
|
||||
|
||||
if (existingAccount) {
|
||||
return NextResponse.json(
|
||||
{ error: 'This account is already linked to another user' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Link the account to the current user
|
||||
const account = await prisma.account.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
type: 'oauth',
|
||||
provider,
|
||||
providerAccountId,
|
||||
access_token,
|
||||
refresh_token,
|
||||
expires_at,
|
||||
token_type: 'Bearer',
|
||||
scope: 'email profile openid',
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Account linked successfully',
|
||||
account: {
|
||||
id: account.id,
|
||||
provider: account.provider,
|
||||
},
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error linking account:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const provider = searchParams.get('provider')
|
||||
|
||||
if (!provider) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Provider is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Remove the linked account
|
||||
const deletedAccount = await prisma.account.deleteMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
provider,
|
||||
},
|
||||
})
|
||||
|
||||
if (deletedAccount.count === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Account not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Account unlinked successfully',
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error unlinking account:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get all linked accounts for the current user
|
||||
const accounts = await prisma.account.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
provider: true,
|
||||
type: true,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
accounts,
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching linked accounts:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
53
app/api/user/addresses/[id]/default/route.ts
Normal file
53
app/api/user/addresses/[id]/default/route.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: addressId } = await params
|
||||
|
||||
// Check if address belongs to user
|
||||
const existingAddress = await prisma.address.findFirst({
|
||||
where: {
|
||||
id: addressId,
|
||||
userId: session.user.id
|
||||
}
|
||||
})
|
||||
|
||||
if (!existingAddress) {
|
||||
return NextResponse.json({ error: 'Address not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Unset all other default addresses for this user
|
||||
await prisma.address.updateMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
isDefault: true
|
||||
},
|
||||
data: { isDefault: false }
|
||||
})
|
||||
|
||||
// Set this address as default
|
||||
const address = await prisma.address.update({
|
||||
where: { id: addressId },
|
||||
data: { isDefault: true }
|
||||
})
|
||||
|
||||
return NextResponse.json({ address })
|
||||
} catch (error) {
|
||||
console.error('Error setting default address:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to set default address' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
122
app/api/user/addresses/[id]/route.ts
Normal file
122
app/api/user/addresses/[id]/route.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: addressId } = await params
|
||||
const data = await request.json()
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
company,
|
||||
address1,
|
||||
address2,
|
||||
city,
|
||||
state,
|
||||
zipCode,
|
||||
country,
|
||||
phone,
|
||||
isDefault,
|
||||
type
|
||||
} = data
|
||||
|
||||
// Check if address belongs to user
|
||||
const existingAddress = await prisma.address.findFirst({
|
||||
where: {
|
||||
id: addressId,
|
||||
userId: session.user.id
|
||||
}
|
||||
})
|
||||
|
||||
if (!existingAddress) {
|
||||
return NextResponse.json({ error: 'Address not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// If setting as default, unset other default addresses
|
||||
if (isDefault) {
|
||||
await prisma.address.updateMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
isDefault: true,
|
||||
id: { not: addressId }
|
||||
},
|
||||
data: { isDefault: false }
|
||||
})
|
||||
}
|
||||
|
||||
const address = await prisma.address.update({
|
||||
where: { id: addressId },
|
||||
data: {
|
||||
firstName,
|
||||
lastName,
|
||||
company,
|
||||
address1,
|
||||
address2,
|
||||
city,
|
||||
state,
|
||||
zipCode,
|
||||
country: country || 'India',
|
||||
phone,
|
||||
isDefault,
|
||||
type
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({ address })
|
||||
} catch (error) {
|
||||
console.error('Error updating address:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update address' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: addressId } = await params
|
||||
|
||||
// Check if address belongs to user
|
||||
const existingAddress = await prisma.address.findFirst({
|
||||
where: {
|
||||
id: addressId,
|
||||
userId: session.user.id
|
||||
}
|
||||
})
|
||||
|
||||
if (!existingAddress) {
|
||||
return NextResponse.json({ error: 'Address not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
await prisma.address.delete({
|
||||
where: { id: addressId }
|
||||
})
|
||||
|
||||
return NextResponse.json({ message: 'Address deleted successfully' })
|
||||
} catch (error) {
|
||||
console.error('Error deleting address:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete address' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
94
app/api/user/addresses/route.ts
Normal file
94
app/api/user/addresses/route.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const addresses = await prisma.address.findMany({
|
||||
where: { userId: session.user.id },
|
||||
orderBy: [
|
||||
{ isDefault: 'desc' },
|
||||
{ createdAt: 'desc' }
|
||||
]
|
||||
})
|
||||
|
||||
return NextResponse.json({ addresses })
|
||||
} catch (error) {
|
||||
console.error('Error fetching addresses:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch addresses' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const data = await request.json()
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
company,
|
||||
address1,
|
||||
address2,
|
||||
city,
|
||||
state,
|
||||
zipCode,
|
||||
country,
|
||||
phone,
|
||||
isDefault,
|
||||
type
|
||||
} = data
|
||||
|
||||
// If setting as default, unset other default addresses
|
||||
if (isDefault) {
|
||||
await prisma.address.updateMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
isDefault: true
|
||||
},
|
||||
data: { isDefault: false }
|
||||
})
|
||||
}
|
||||
|
||||
const address = await prisma.address.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
firstName,
|
||||
lastName,
|
||||
company,
|
||||
address1,
|
||||
address2,
|
||||
city,
|
||||
state,
|
||||
zipCode,
|
||||
country: country || 'India',
|
||||
phone,
|
||||
isDefault,
|
||||
type
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({ address })
|
||||
} catch (error) {
|
||||
console.error('Error creating address:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create address' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
58
app/api/user/change-password/route.ts
Normal file
58
app/api/user/change-password/route.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { currentPassword, newPassword } = await request.json()
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
return NextResponse.json({ error: 'All fields are required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
return NextResponse.json({ error: 'Password must be at least 6 characters long' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Get current user with password
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: { password: true }
|
||||
})
|
||||
|
||||
if (!user || !user.password) {
|
||||
return NextResponse.json({ error: 'User not found or password not set' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
const isCurrentPasswordValid = await bcrypt.compare(currentPassword, user.password)
|
||||
|
||||
if (!isCurrentPasswordValid) {
|
||||
return NextResponse.json({ error: 'Current password is incorrect' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
const hashedNewPassword = await bcrypt.hash(newPassword, 12)
|
||||
|
||||
// Update password
|
||||
await prisma.user.update({
|
||||
where: { id: session.user.id },
|
||||
data: { password: hashedNewPassword }
|
||||
})
|
||||
|
||||
return NextResponse.json({ message: 'Password updated successfully' })
|
||||
} catch (error) {
|
||||
console.error('Error changing password:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to change password' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
83
app/api/user/profile/route.ts
Normal file
83
app/api/user/profile/route.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
phone: true,
|
||||
address: true,
|
||||
image: true,
|
||||
role: true,
|
||||
referralCode: true,
|
||||
isActive: true,
|
||||
joinedAt: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json(user)
|
||||
} catch (error) {
|
||||
console.error('Error fetching user profile:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch user profile' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const data = await request.json()
|
||||
const { name, phone, address } = data
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: session.user.id },
|
||||
data: {
|
||||
name: name || undefined,
|
||||
phone: phone || undefined,
|
||||
address: address || undefined
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
phone: true,
|
||||
address: true,
|
||||
role: true,
|
||||
referralCode: true,
|
||||
isActive: true,
|
||||
joinedAt: true
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json(updatedUser)
|
||||
} catch (error) {
|
||||
console.error('Error updating user profile:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update user profile' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
28
app/api/version/route.ts
Normal file
28
app/api/version/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Get version info from environment or package.json
|
||||
const versionInfo = {
|
||||
version: process.env.npm_package_version || '1.0.0',
|
||||
buildTime: process.env.BUILD_TIME || new Date().toISOString(),
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
commitHash: process.env.VERCEL_GIT_COMMIT_SHA || process.env.GIT_COMMIT_SHA || 'unknown',
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
|
||||
return NextResponse.json(versionInfo, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Version API error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get version info' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
152
app/api/wholesaler/register/route.ts
Normal file
152
app/api/wholesaler/register/route.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { EmailService } from '@/lib/email'
|
||||
|
||||
interface WholesalerRegistrationData {
|
||||
// Business Information
|
||||
businessName: string
|
||||
businessType: string
|
||||
gstNumber: string
|
||||
panNumber: string
|
||||
|
||||
// Personal Information
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
phone: string
|
||||
|
||||
// Address Information
|
||||
address: string
|
||||
city: string
|
||||
state: string
|
||||
zipCode: string
|
||||
|
||||
// Business Details
|
||||
experience: string
|
||||
expectedOrderVolume: string
|
||||
productCategories: string
|
||||
businessDescription: string
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const data: WholesalerRegistrationData = await request.json()
|
||||
|
||||
// Validate required fields
|
||||
const requiredFields = ['businessName', 'firstName', 'lastName', 'email', 'phone', 'address', 'city', 'state', 'zipCode']
|
||||
for (const field of requiredFields) {
|
||||
if (!data[field as keyof WholesalerRegistrationData]) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: `${field} is required` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: data.email }
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Email already registered' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate a temporary password
|
||||
const tempPassword = Math.random().toString(36).slice(-8)
|
||||
const hashedPassword = await bcrypt.hash(tempPassword, 10)
|
||||
|
||||
// Create user with WHOLESALER role
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
name: `${data.firstName} ${data.lastName}`,
|
||||
email: data.email,
|
||||
password: hashedPassword,
|
||||
role: 'WHOLESALER',
|
||||
phone: data.phone,
|
||||
address: data.address,
|
||||
isActive: true,
|
||||
joinedAt: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
// Create address record
|
||||
await prisma.address.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
company: data.businessName,
|
||||
address1: data.address,
|
||||
city: data.city,
|
||||
state: data.state,
|
||||
zipCode: data.zipCode,
|
||||
country: 'India',
|
||||
phone: data.phone,
|
||||
isDefault: true,
|
||||
type: 'WORK'
|
||||
}
|
||||
})
|
||||
|
||||
// Create form response for admin tracking
|
||||
await prisma.formResponse.create({
|
||||
data: {
|
||||
formId: 'wholesaler_registration',
|
||||
data: {
|
||||
...data,
|
||||
userId: user.id,
|
||||
registrationDate: new Date().toISOString()
|
||||
},
|
||||
status: 'new',
|
||||
userId: user.id
|
||||
}
|
||||
})
|
||||
|
||||
// Send email notifications
|
||||
const emailService = new EmailService()
|
||||
|
||||
// Send welcome email to wholesaler
|
||||
await emailService.sendWholesalerWelcomeEmail({
|
||||
to: user.email,
|
||||
name: user.name || 'Wholesaler',
|
||||
email: user.email,
|
||||
password: tempPassword,
|
||||
businessName: data.businessName
|
||||
})
|
||||
|
||||
// Send admin notification
|
||||
await emailService.sendWholesalerAdminNotification({
|
||||
wholesalerName: user.name || 'Unknown',
|
||||
wholesalerEmail: user.email,
|
||||
businessName: data.businessName,
|
||||
businessType: data.businessType,
|
||||
phone: data.phone,
|
||||
expectedVolume: data.expectedOrderVolume,
|
||||
registrationDate: new Date().toLocaleDateString()
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Wholesaler registration successful',
|
||||
loginCredentials: {
|
||||
email: user.email,
|
||||
password: tempPassword
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Wholesaler registration error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'Registration failed',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user