first commit
This commit is contained in:
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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user