first commit

This commit is contained in:
2026-01-17 14:17:42 +05:30
commit 0f194eb9e7
328 changed files with 73544 additions and 0 deletions

View File

@@ -0,0 +1,301 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Search, Plus, Edit, Trash2, MoreHorizontal, Download } from 'lucide-react'
import { toast } from 'sonner'
import Link from 'next/link'
import Image from 'next/image'
import { CategoryFormDialog } from '@/components/admin/CategoryFormDialog'
interface Category {
id: string
name: string
description: string
image: string | null
isActive: boolean
createdAt: string
_count: {
products: number
}
}
export default function AdminCategoriesPage() {
const [categories, setCategories] = useState<Category[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState('')
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
const [bulkActionLoading, setBulkActionLoading] = useState(false)
const fetchCategories = useCallback(async () => {
try {
setLoading(true)
const params = new URLSearchParams()
if (search) params.append('search', search)
const response = await fetch(`/api/admin/categories?${params}`)
const data = await response.json()
setCategories(data || [])
} catch (error) {
console.error('Error fetching categories:', error)
toast.error('Failed to load categories')
} finally {
setLoading(false)
}
}, [search])
useEffect(() => {
fetchCategories()
}, [fetchCategories])
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedCategories(categories.map(c => c.id))
} else {
setSelectedCategories([])
}
}
const handleSelectCategory = (categoryId: string, checked: boolean) => {
if (checked) {
setSelectedCategories(prev => [...prev, categoryId])
} else {
setSelectedCategories(prev => prev.filter(id => id !== categoryId))
}
}
const handleBulkAction = async (action: 'activate' | 'deactivate' | 'delete') => {
if (selectedCategories.length === 0) {
toast.error('Please select at least one category')
return
}
setBulkActionLoading(true)
try {
const response = await fetch('/api/admin/categories/bulk', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
categoryIds: selectedCategories,
action
})
})
if (!response.ok) throw new Error('Bulk action failed')
toast.success(`Successfully ${action}d ${selectedCategories.length} categories`)
setSelectedCategories([])
fetchCategories()
} catch (error) {
toast.error(`Failed to ${action} categories`)
} finally {
setBulkActionLoading(false)
}
}
const handleDeleteCategory = async (categoryId: string) => {
if (!confirm('Are you sure you want to delete this category?')) return
try {
const response = await fetch(`/api/admin/categories/${categoryId}`, {
method: 'DELETE'
})
if (!response.ok) throw new Error('Delete failed')
toast.success('Category deleted successfully')
fetchCategories()
} catch (error) {
toast.error('Failed to delete category')
}
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Categories</h1>
<p className="text-gray-600">Manage your product categories</p>
</div>
<CategoryFormDialog onSuccess={fetchCategories} />
</div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>All Categories</CardTitle>
<div className="flex items-center space-x-2">
{selectedCategories.length > 0 && (
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-500">
{selectedCategories.length} selected
</span>
<Button
variant="outline"
size="sm"
onClick={() => handleBulkAction('activate')}
disabled={bulkActionLoading}
>
Activate
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleBulkAction('deactivate')}
disabled={bulkActionLoading}
>
Deactivate
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleBulkAction('delete')}
disabled={bulkActionLoading}
className="text-red-600"
>
Delete
</Button>
</div>
)}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search categories..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 w-64"
/>
</div>
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
Export
</Button>
</div>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="text-center py-8">Loading...</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={selectedCategories.length === categories.length && categories.length > 0}
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>Category</TableHead>
<TableHead>Description</TableHead>
<TableHead>Products</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{categories.map((category) => (
<TableRow key={category.id}>
<TableCell>
<Checkbox
checked={selectedCategories.includes(category.id)}
onCheckedChange={(checked) => handleSelectCategory(category.id, !!checked)}
/>
</TableCell>
<TableCell>
<div className="flex items-center space-x-3">
{category.image ? (
<Image
src={category.image}
alt={category.name}
width={40}
height={40}
className="rounded-lg object-cover"
/>
) : (
<div className="w-10 h-10 bg-gray-200 rounded-lg flex items-center justify-center">
<span className="text-gray-400 text-xs">No Image</span>
</div>
)}
<div>
<p className="font-medium">{category.name}</p>
</div>
</div>
</TableCell>
<TableCell>
<p className="text-sm text-gray-600 max-w-xs truncate">
{category.description || 'No description'}
</p>
</TableCell>
<TableCell>
<Badge variant="secondary">
{category._count.products} products
</Badge>
</TableCell>
<TableCell>
<Badge variant={category.isActive ? 'default' : 'secondary'}>
{category.isActive ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell>
{new Date(category.createdAt).toLocaleDateString()}
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<CategoryFormDialog
category={category}
mode="edit"
onSuccess={fetchCategories}
trigger={
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<Edit className="h-4 w-4 mr-2" />
Edit
</DropdownMenuItem>
}
/>
<DropdownMenuItem
onClick={() => handleDeleteCategory(category.id)}
className="text-red-600"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,525 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
DollarSign,
TrendingUp,
Users,
Settings,
Search,
Filter,
CheckCircle,
Clock,
XCircle,
Edit,
Plus
} from 'lucide-react'
import { motion } from 'framer-motion'
import { toast } from 'sonner'
interface Commission {
id: string
amount: number
level: number
type: 'REFERRAL' | 'LEVEL' | 'BONUS'
status: 'PENDING' | 'APPROVED' | 'PAID' | 'CANCELLED'
createdAt: string
user: {
id: string
name: string
email: string
}
fromUser: {
id: string
name: string
email: string
}
order?: {
id: string
total: number
}
}
interface CommissionStats {
totalCommissions: number
pendingAmount: number
approvedAmount: number
paidAmount: number
thisMonthCommissions: number
averageCommission: number
}
interface CommissionSetting {
id: string
level: number
percentage: number
isActive: boolean
}
const statusConfig = {
PENDING: { label: 'Pending', color: 'bg-yellow-100 text-yellow-800', icon: Clock },
APPROVED: { label: 'Approved', color: 'bg-blue-100 text-blue-800', icon: CheckCircle },
PAID: { label: 'Paid', color: 'bg-green-100 text-green-800', icon: CheckCircle },
CANCELLED: { label: 'Cancelled', color: 'bg-red-100 text-red-800', icon: XCircle }
}
export default function AdminCommissionsPage() {
const [commissions, setCommissions] = useState<Commission[]>([])
const [stats, setStats] = useState<CommissionStats | null>(null)
const [settings, setSettings] = useState<CommissionSetting[]>([])
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState({
status: 'all',
type: 'all',
level: 'all',
search: ''
})
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false)
const [editingSetting, setEditingSetting] = useState<CommissionSetting | null>(null)
const fetchData = useCallback(async () => {
try {
setLoading(true)
const [commissionsRes, statsRes, settingsRes] = await Promise.all([
fetch('/api/admin/commissions'),
fetch('/api/admin/commissions/stats'),
fetch('/api/admin/commissions/settings')
])
// Check if responses are ok and contain valid JSON
const commissionsData = commissionsRes.ok ? await commissionsRes.json() : { commissions: [] }
const statsData = statsRes.ok ? await statsRes.json() : {
totalCommissions: 0,
pendingAmount: 0,
approvedAmount: 0,
paidAmount: 0,
thisMonthCommissions: 0,
averageCommission: 0
}
const settingsData = settingsRes.ok ? await settingsRes.json() : { settings: [] }
setCommissions(commissionsData.commissions || [])
setStats(statsData)
setSettings(settingsData.settings || [])
} catch (error) {
console.error('Error fetching commission data:', error)
toast.error('Failed to load commission data')
// Set fallback data
setCommissions([])
setStats({
totalCommissions: 0,
pendingAmount: 0,
approvedAmount: 0,
paidAmount: 0,
thisMonthCommissions: 0,
averageCommission: 0
})
setSettings([])
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchData()
}, [fetchData])
const handleStatusUpdate = async (commissionId: string, newStatus: string) => {
try {
const response = await fetch(`/api/admin/commissions/${commissionId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus })
})
if (!response.ok) throw new Error('Failed to update commission')
toast.success('Commission status updated successfully')
fetchData()
} catch (error) {
console.error('Error updating commission:', error)
toast.error('Failed to update commission status')
}
}
const handleSettingUpdate = async (setting: CommissionSetting) => {
try {
const response = await fetch(`/api/admin/commissions/settings/${setting.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
level: setting.level,
percentage: setting.percentage,
isActive: setting.isActive
})
})
if (!response.ok) throw new Error('Failed to update setting')
toast.success('Commission setting updated successfully')
fetchData()
setEditingSetting(null)
setSettingsDialogOpen(false)
} catch (error) {
console.error('Error updating setting:', error)
toast.error('Failed to update commission setting')
}
}
const filteredCommissions = commissions.filter(commission => {
const matchesStatus = filter.status === 'all' || commission.status === filter.status
const matchesType = filter.type === 'all' || commission.type === filter.type
const matchesLevel = filter.level === 'all' || commission.level.toString() === filter.level
const matchesSearch = !filter.search ||
commission.user.name.toLowerCase().includes(filter.search.toLowerCase()) ||
commission.user.email.toLowerCase().includes(filter.search.toLowerCase()) ||
commission.fromUser.name.toLowerCase().includes(filter.search.toLowerCase())
return matchesStatus && matchesType && matchesLevel && matchesSearch
})
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
<div className="w-16 h-16 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100 p-6">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6"
>
<div>
<h1 className="text-4xl font-bold bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text text-transparent">
Commission Management
</h1>
<p className="text-gray-600 mt-2">Monitor and manage all commission transactions</p>
</div>
<Dialog open={settingsDialogOpen} onOpenChange={setSettingsDialogOpen}>
<DialogTrigger asChild>
<Button className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 shadow-lg">
<Settings className="h-4 w-4 mr-2" />
Commission Settings
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Commission Settings</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{settings.map((setting) => (
<div key={setting.id} className="flex items-center justify-between p-4 border rounded-lg">
<div>
<p className="font-medium">Level {setting.level}</p>
<p className="text-sm text-gray-500">{setting.percentage}% commission</p>
</div>
<div className="flex items-center space-x-2">
<Badge variant={setting.isActive ? 'default' : 'secondary'}>
{setting.isActive ? 'Active' : 'Inactive'}
</Badge>
<Button
variant="outline"
size="sm"
onClick={() => setEditingSetting(setting)}
>
<Edit className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</DialogContent>
</Dialog>
</motion.div>
{/* Stats Cards */}
{stats && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"
>
<Card className="bg-gradient-to-r from-blue-500 to-blue-600 text-white border-0 shadow-lg">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100 text-sm">Total Commissions</p>
<p className="text-2xl font-bold">{stats.totalCommissions}</p>
</div>
<Users className="h-8 w-8 text-blue-200" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-yellow-500 to-yellow-600 text-white border-0 shadow-lg">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-yellow-100 text-sm">Pending Amount</p>
<p className="text-2xl font-bold">{stats.pendingAmount.toFixed(0)}</p>
</div>
<Clock className="h-8 w-8 text-yellow-200" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-green-500 to-green-600 text-white border-0 shadow-lg">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-green-100 text-sm">Paid Amount</p>
<p className="text-2xl font-bold">{stats.paidAmount.toFixed(0)}</p>
</div>
<CheckCircle className="h-8 w-8 text-green-200" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-purple-500 to-purple-600 text-white border-0 shadow-lg">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-purple-100 text-sm">Avg Commission</p>
<p className="text-2xl font-bold">{stats.averageCommission.toFixed(0)}</p>
</div>
<TrendingUp className="h-8 w-8 text-purple-200" />
</div>
</CardContent>
</Card>
</motion.div>
)}
{/* Main Commission Table */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<Card className="bg-white/90 backdrop-blur-sm border-0 shadow-xl">
<CardHeader>
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
<CardTitle className="text-2xl font-bold">All Commissions</CardTitle>
<div className="flex flex-wrap items-center gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search users..."
value={filter.search}
onChange={(e) => setFilter(prev => ({ ...prev, search: e.target.value }))}
className="pl-10 w-64 bg-white shadow-sm"
/>
</div>
<Select value={filter.status} onValueChange={(value) => setFilter(prev => ({ ...prev, status: value }))}>
<SelectTrigger className="w-40 bg-white shadow-sm">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="PENDING">Pending</SelectItem>
<SelectItem value="APPROVED">Approved</SelectItem>
<SelectItem value="PAID">Paid</SelectItem>
<SelectItem value="CANCELLED">Cancelled</SelectItem>
</SelectContent>
</Select>
<Select value={filter.type} onValueChange={(value) => setFilter(prev => ({ ...prev, type: value }))}>
<SelectTrigger className="w-40 bg-white shadow-sm">
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="REFERRAL">Referral</SelectItem>
<SelectItem value="LEVEL">Level</SelectItem>
<SelectItem value="BONUS">Bonus</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
<div className="overflow-hidden rounded-lg border border-gray-200">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="font-semibold">Recipient</TableHead>
<TableHead className="font-semibold">From User</TableHead>
<TableHead className="font-semibold">Amount</TableHead>
<TableHead className="font-semibold">Level</TableHead>
<TableHead className="font-semibold">Type</TableHead>
<TableHead className="font-semibold">Status</TableHead>
<TableHead className="font-semibold">Date</TableHead>
<TableHead className="font-semibold">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredCommissions.map((commission, index) => (
<motion.tr
key={commission.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className="hover:bg-gray-50 transition-colors"
>
<TableCell>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-r from-blue-500 to-purple-600 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-medium">
{commission.user.name?.charAt(0) || 'U'}
</span>
</div>
<div>
<p className="font-medium text-gray-900">{commission.user.name}</p>
<p className="text-sm text-gray-500">{commission.user.email}</p>
</div>
</div>
</TableCell>
<TableCell>
<div>
<p className="font-medium text-gray-900">{commission.fromUser.name}</p>
<p className="text-sm text-gray-500">{commission.fromUser.email}</p>
</div>
</TableCell>
<TableCell>
<span className="font-bold text-lg bg-gradient-to-r from-green-600 to-emerald-600 bg-clip-text text-transparent">
{commission.amount.toFixed(2)}
</span>
</TableCell>
<TableCell>
<Badge variant="outline">Level {commission.level}</Badge>
</TableCell>
<TableCell>
<Badge variant="secondary">{commission.type}</Badge>
</TableCell>
<TableCell>
<Badge className={statusConfig[commission.status].color}>
{statusConfig[commission.status].label}
</Badge>
</TableCell>
<TableCell>
<p className="text-sm">
{new Date(commission.createdAt).toLocaleDateString()}
</p>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
{commission.status === 'PENDING' && (
<>
<Button
size="sm"
variant="outline"
onClick={() => handleStatusUpdate(commission.id, 'APPROVED')}
className="text-blue-600 hover:bg-blue-50"
>
Approve
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleStatusUpdate(commission.id, 'CANCELLED')}
className="text-red-600 hover:bg-red-50"
>
Reject
</Button>
</>
)}
{commission.status === 'APPROVED' && (
<Button
size="sm"
variant="outline"
onClick={() => handleStatusUpdate(commission.id, 'PAID')}
className="text-green-600 hover:bg-green-50"
>
Mark Paid
</Button>
)}
</div>
</TableCell>
</motion.tr>
))}
</TableBody>
</Table>
</div>
{filteredCommissions.length === 0 && (
<div className="text-center py-12">
<DollarSign className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No commissions found</h3>
<p className="text-gray-500">Try adjusting your search or filter criteria</p>
</div>
)}
</CardContent>
</Card>
</motion.div>
{/* Edit Setting Dialog */}
{editingSetting && (
<Dialog open={!!editingSetting} onOpenChange={() => setEditingSetting(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Commission Setting</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Level</Label>
<Input
type="number"
value={editingSetting.level}
onChange={(e) => setEditingSetting(prev => prev ? { ...prev, level: parseInt(e.target.value) } : null)}
/>
</div>
<div>
<Label>Percentage (%)</Label>
<Input
type="number"
step="0.1"
value={editingSetting.percentage}
onChange={(e) => setEditingSetting(prev => prev ? { ...prev, percentage: parseFloat(e.target.value) } : null)}
/>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="isActive"
checked={editingSetting.isActive}
onChange={(e) => setEditingSetting(prev => prev ? { ...prev, isActive: e.target.checked } : null)}
/>
<Label htmlFor="isActive">Active</Label>
</div>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={() => setEditingSetting(null)}>
Cancel
</Button>
<Button onClick={() => handleSettingUpdate(editingSetting)}>
Save Changes
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)}
</div>
</div>
)
}

646
app/admin/forms/page.tsx Normal file
View File

@@ -0,0 +1,646 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Input } from '@/components/ui/input'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Mail,
Search,
Filter,
Eye,
Calendar,
User,
Building,
Phone,
MessageSquare,
ExternalLink
} from 'lucide-react'
import { toast } from 'sonner'
interface FormResponse {
id: string
formId: string
data: any // Changed from specific interface to any to handle different form types
metadata?: {
ipAddress?: string
userAgent?: string
referrer?: string
timestamp?: string
}
status: string
createdAt: string
updatedAt: string
}
interface Pagination {
page: number
limit: number
total: number
pages: number
}
export default function AdminFormsPage() {
const [forms, setForms] = useState<FormResponse[]>([])
const [loading, setLoading] = useState(true)
const [selectedForm, setSelectedForm] = useState<FormResponse | null>(null)
const [showDetails, setShowDetails] = useState(false)
const [pagination, setPagination] = useState<Pagination>({
page: 1,
limit: 10,
total: 0,
pages: 0
})
// Filters
const [statusFilter, setStatusFilter] = useState('all')
const [formTypeFilter, setFormTypeFilter] = useState('all')
const [searchQuery, setSearchQuery] = useState('')
const fetchForms = useCallback(async () => {
try {
setLoading(true)
const params = new URLSearchParams({
page: pagination.page.toString(),
limit: pagination.limit.toString(),
})
if (statusFilter !== 'all') {
params.append('status', statusFilter)
}
if (formTypeFilter !== 'all') {
params.append('formId', formTypeFilter)
}
const response = await fetch(`/api/forms?${params}`)
const result = await response.json()
if (result.success) {
setForms(result.data)
setPagination(result.pagination)
} else {
toast.error('Failed to fetch form responses')
}
} catch (error) {
console.error('Error fetching forms:', error)
toast.error('Failed to fetch form responses')
} finally {
setLoading(false)
}
}, [pagination.page, pagination.limit, statusFilter, formTypeFilter])
useEffect(() => {
fetchForms()
}, [fetchForms])
const updateFormStatus = async (formId: string, newStatus: string) => {
try {
const response = await fetch(`/api/forms/${formId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ status: newStatus })
})
if (response.ok) {
toast.success('Status updated successfully')
fetchForms()
if (selectedForm && selectedForm.id === formId) {
setSelectedForm({ ...selectedForm, status: newStatus })
}
} else {
toast.error('Failed to update status')
}
} catch (error) {
console.error('Error updating status:', error)
toast.error('Failed to update status')
}
}
const getStatusBadge = (status: string) => {
const statusColors = {
new: 'bg-blue-100 text-blue-800',
in_progress: 'bg-yellow-100 text-yellow-800',
resolved: 'bg-green-100 text-green-800',
closed: 'bg-gray-100 text-gray-800'
}
return (
<Badge className={statusColors[status as keyof typeof statusColors] || 'bg-gray-100 text-gray-800'}>
{status.replace('_', ' ').toUpperCase()}
</Badge>
)
}
const getFormTypeBadge = (formId: string) => {
const typeColors = {
contact: 'bg-purple-100 text-purple-800',
partnership: 'bg-indigo-100 text-indigo-800',
bulk_inquiry: 'bg-orange-100 text-orange-800',
support: 'bg-teal-100 text-teal-800'
}
return (
<Badge variant="outline" className={typeColors[formId as keyof typeof typeColors]}>
{formId.replace('_', ' ').toUpperCase()}
</Badge>
)
}
const filteredForms = forms.filter(form => {
if (searchQuery) {
const query = searchQuery.toLowerCase()
const data = form.data
// Handle different form types
if (form.formId === 'partnership_application') {
return (
data.firstName?.toLowerCase().includes(query) ||
data.lastName?.toLowerCase().includes(query) ||
data.email?.toLowerCase().includes(query) ||
data.businessName?.toLowerCase().includes(query) ||
data.partnershipTier?.toLowerCase().includes(query) ||
data.motivation?.toLowerCase().includes(query)
)
} else {
// Contact forms and other types
return (
data.name?.toLowerCase().includes(query) ||
data.email?.toLowerCase().includes(query) ||
data.message?.toLowerCase().includes(query) ||
data.subject?.toLowerCase().includes(query)
)
}
}
return true
})
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Form Responses</h1>
<p className="text-muted-foreground">
Manage and respond to customer inquiries and form submissions
</p>
</div>
</div>
{/* Filters */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Filter className="h-5 w-5" />
Filters
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<Search className="h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search by name, email, or message..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-64"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="new">New</SelectItem>
<SelectItem value="in_progress">In Progress</SelectItem>
<SelectItem value="resolved">Resolved</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
</SelectContent>
</Select>
<Select value={formTypeFilter} onValueChange={setFormTypeFilter}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Form Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="contact">Contact</SelectItem>
<SelectItem value="partnership">Partnership</SelectItem>
<SelectItem value="bulk_inquiry">Bulk Inquiry</SelectItem>
<SelectItem value="support">Support</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Form Responses Table */}
<Card>
<CardHeader>
<CardTitle>
Form Responses ({pagination.total})
</CardTitle>
<CardDescription>
Click on any row to view detailed information and respond to inquiries
</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
) : (
<div className="space-y-4">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Type</TableHead>
<TableHead>Status</TableHead>
<TableHead>Subject</TableHead>
<TableHead>Date</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredForms.map((form) => {
// Handle different form types for display
const displayName = form.formId === 'partnership_application'
? `${form.data.firstName || ''} ${form.data.lastName || ''}`.trim()
: form.data.name || 'Unknown'
const displayEmail = form.data.email || 'No email'
const displaySubject = form.formId === 'partnership_application'
? `${form.data.partnershipTier || 'Unknown'} Partnership Application`
: form.data.subject || form.data.inquiryType || 'No subject'
return (
<TableRow key={form.id} className="cursor-pointer hover:bg-muted/50">
<TableCell className="font-medium">{displayName}</TableCell>
<TableCell>{displayEmail}</TableCell>
<TableCell>{getFormTypeBadge(form.formId)}</TableCell>
<TableCell>{getStatusBadge(form.status)}</TableCell>
<TableCell className="max-w-xs truncate">
{displaySubject}
</TableCell>
<TableCell>
{new Date(form.createdAt).toLocaleDateString()}
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedForm(form)
setShowDetails(true)
}}
>
<Eye className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
{/* Pagination */}
{pagination.pages > 1 && (
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
Showing {((pagination.page - 1) * pagination.limit) + 1} to{' '}
{Math.min(pagination.page * pagination.limit, pagination.total)} of{' '}
{pagination.total} entries
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setPagination(p => ({ ...p, page: p.page - 1 }))}
disabled={pagination.page === 1}
>
Previous
</Button>
<span className="text-sm">
Page {pagination.page} of {pagination.pages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setPagination(p => ({ ...p, page: p.page + 1 }))}
disabled={pagination.page === pagination.pages}
>
Next
</Button>
</div>
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* Form Details Dialog */}
<Dialog open={showDetails} onOpenChange={setShowDetails}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Mail className="h-5 w-5" />
Form Response Details
</DialogTitle>
<DialogDescription>
View and manage this form submission
</DialogDescription>
</DialogHeader>
{selectedForm && (
<div className="space-y-6">
{/* Header Info */}
<div className="flex items-center justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
{getFormTypeBadge(selectedForm.formId)}
{getStatusBadge(selectedForm.status)}
</div>
<p className="text-sm text-muted-foreground">
Submitted on {new Date(selectedForm.createdAt).toLocaleDateString()} at{' '}
{new Date(selectedForm.createdAt).toLocaleTimeString()}
</p>
</div>
<Select
value={selectedForm.status}
onValueChange={(newStatus) => updateFormStatus(selectedForm.id, newStatus)}
>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="new">New</SelectItem>
<SelectItem value="in_progress">In Progress</SelectItem>
<SelectItem value="resolved">Resolved</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
</SelectContent>
</Select>
</div>
{/* Contact Information */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Contact Information</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Name:</span>
<span>
{selectedForm.formId === 'partnership_application'
? `${selectedForm.data.firstName || ''} ${selectedForm.data.lastName || ''}`.trim()
: selectedForm.data.name || 'Unknown'
}
</span>
</div>
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Email:</span>
<a
href={`mailto:${selectedForm.data.email}`}
className="text-blue-600 hover:underline flex items-center gap-1"
>
{selectedForm.data.email}
<ExternalLink className="h-3 w-3" />
</a>
</div>
{selectedForm.data.phone && (
<div className="flex items-center gap-2">
<Phone className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Phone:</span>
<a
href={`tel:${selectedForm.data.phone}`}
className="text-blue-600 hover:underline"
>
{selectedForm.data.phone}
</a>
</div>
)}
{/* Show business info for partnership applications */}
{selectedForm.formId === 'partnership_application' && selectedForm.data.businessName && (
<div className="flex items-center gap-2">
<Building className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Business:</span>
<span>{selectedForm.data.businessName}</span>
</div>
)}
{/* Show company info for other forms */}
{selectedForm.formId !== 'partnership_application' && selectedForm.data.company && (
<div className="flex items-center gap-2">
<Building className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Company:</span>
<span>{selectedForm.data.company}</span>
</div>
)}
</CardContent>
</Card>
{/* Message/Details Content */}
{selectedForm.formId === 'partnership_application' ? (
// Partnership Application Details
<>
<Card>
<CardHeader>
<CardTitle className="text-lg">Partnership Details</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="font-medium">Partnership Tier:</span>
<Badge variant="outline" className="ml-2">{selectedForm.data.partnershipTier}</Badge>
</div>
<div>
<span className="font-medium">Expected Customers:</span>
<span className="ml-2">{selectedForm.data.expectedCustomers || 'Not specified'}</span>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<span className="font-medium">Business Type:</span>
<span className="ml-2">{selectedForm.data.businessType || 'Not specified'}</span>
</div>
<div>
<span className="font-medium">Experience:</span>
<span className="ml-2">{selectedForm.data.experience || 'Not specified'}</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">Address Information</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div><span className="font-medium">Address:</span> {selectedForm.data.address || 'Not provided'}</div>
<div className="grid grid-cols-3 gap-4">
<div><span className="font-medium">City:</span> {selectedForm.data.city || 'Not provided'}</div>
<div><span className="font-medium">State:</span> {selectedForm.data.state || 'Not provided'}</div>
<div><span className="font-medium">Zip Code:</span> {selectedForm.data.zipCode || 'Not provided'}</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">Motivation & Plans</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<span className="font-medium">Motivation:</span>
<div className="bg-muted/50 p-3 rounded-lg mt-2">
<p className="whitespace-pre-wrap">{selectedForm.data.motivation || 'Not provided'}</p>
</div>
</div>
<div>
<span className="font-medium">Marketing Plan:</span>
<div className="bg-muted/50 p-3 rounded-lg mt-2">
<p className="whitespace-pre-wrap">{selectedForm.data.marketingPlan || 'Not provided'}</p>
</div>
</div>
</CardContent>
</Card>
</>
) : (
// Regular Contact Form Message
<Card>
<CardHeader>
<CardTitle className="text-lg">Message</CardTitle>
{selectedForm.data.subject && (
<CardDescription className="font-medium">
Subject: {selectedForm.data.subject}
</CardDescription>
)}
</CardHeader>
<CardContent>
<div className="bg-muted/50 p-4 rounded-lg">
<p className="whitespace-pre-wrap">{selectedForm.data.message}</p>
</div>
</CardContent>
</Card>
)}
{/* Additional Information */}
{(selectedForm.data.inquiryType || selectedForm.data.formSource) && (
<Card>
<CardHeader>
<CardTitle className="text-lg">Additional Information</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{selectedForm.data.inquiryType && (
<div className="flex items-center gap-2">
<span className="font-medium">Inquiry Type:</span>
<Badge variant="outline">{selectedForm.data.inquiryType}</Badge>
</div>
)}
{selectedForm.data.formSource && (
<div className="flex items-center gap-2">
<span className="font-medium">Form Source:</span>
<span>{selectedForm.data.formSource}</span>
</div>
)}
</CardContent>
</Card>
)}
{/* Metadata */}
{selectedForm.metadata && (
<Card>
<CardHeader>
<CardTitle className="text-lg">Technical Details</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
{selectedForm.metadata.ipAddress && (
<div>
<span className="font-medium">IP Address:</span> {selectedForm.metadata.ipAddress}
</div>
)}
{selectedForm.metadata.referrer && (
<div>
<span className="font-medium">Referrer:</span> {selectedForm.metadata.referrer}
</div>
)}
{selectedForm.metadata.userAgent && (
<div>
<span className="font-medium">User Agent:</span>
<div className="mt-1 p-2 bg-muted/50 rounded text-xs break-all">
{selectedForm.metadata.userAgent}
</div>
</div>
)}
</CardContent>
</Card>
)}
{/* Actions */}
<div className="flex gap-3 pt-4 border-t">
<Button
onClick={() => window.open(`mailto:${selectedForm.data.email}?subject=Re: ${selectedForm.data.subject || 'Your inquiry'}`)}
className="flex items-center gap-2"
>
<Mail className="h-4 w-4" />
Reply via Email
</Button>
<Button
variant="outline"
onClick={() => updateFormStatus(selectedForm.id, 'resolved')}
disabled={selectedForm.status === 'resolved'}
>
Mark as Resolved
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
)
}

28
app/admin/layout.tsx Normal file
View File

@@ -0,0 +1,28 @@
import { redirect } from 'next/navigation'
import { auth } from '@/auth'
import { AdminSidebar } from '@/components/admin/AdminSidebar'
import { AdminHeader } from '@/components/admin/AdminHeader'
export default async function AdminLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
redirect('/auth/signin?callbackUrl=/admin')
}
return (
<div className="min-h-screen bg-gray-50">
<AdminHeader user={session.user} />
<div className="flex">
<AdminSidebar />
<main className="flex-1 p-6">
{children}
</main>
</div>
</div>
)
}

135
app/admin/loading.tsx Normal file
View File

@@ -0,0 +1,135 @@
'use client'
import { motion } from 'framer-motion'
import { Settings, Shield, BarChart3 } from 'lucide-react'
export default function AdminLoading() {
return (
<div className="min-h-[70vh] flex items-center justify-center px-4">
<div className="text-center">
{/* Admin Loading Animation */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
className="mb-6"
>
<div className="relative mx-auto w-24 h-24 mb-4">
{/* Outer ring */}
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 3,
repeat: Infinity,
ease: "linear"
}}
className="absolute inset-0 border-3 border-blue-200 border-t-blue-600 rounded-full"
/>
{/* Inner ring */}
<motion.div
animate={{ rotate: -360 }}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
className="absolute inset-3 border-2 border-purple-200 border-b-purple-500 rounded-full"
/>
{/* Center icon */}
<motion.div
animate={{
scale: [1, 1.15, 1],
rotate: [0, 180, 360]
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut"
}}
className="absolute inset-6 flex items-center justify-center bg-gradient-to-br from-blue-50 to-purple-50 rounded-full shadow-sm"
>
<Settings className="w-6 h-6 text-blue-600" />
</motion.div>
</div>
</motion.div>
{/* Loading Text */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="mb-4"
>
<div className="flex items-center justify-center space-x-2 text-blue-600 font-semibold text-lg">
<Shield className="w-5 h-5" />
<span>Loading Admin Panel</span>
</div>
<p className="text-slate-500 text-sm mt-1">
Preparing dashboard data...
</p>
</motion.div>
{/* Progress indicators */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.4 }}
className="flex justify-center space-x-2"
>
{[0, 1, 2, 3].map((index) => (
<motion.div
key={index}
className="w-2 h-2 bg-blue-500 rounded-full"
animate={{
scale: [1, 1.4, 1],
opacity: [0.3, 1, 0.3]
}}
transition={{
duration: 1.8,
repeat: Infinity,
delay: index * 0.2,
ease: "easeInOut"
}}
/>
))}
</motion.div>
{/* Stats preview */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.6 }}
className="mt-8 grid grid-cols-3 gap-4 max-w-sm mx-auto"
>
{[
{ icon: BarChart3, label: 'Analytics' },
{ icon: Settings, label: 'Settings' },
{ icon: Shield, label: 'Security' }
].map((item, index) => {
const IconComponent = item.icon
return (
<motion.div
key={item.label}
animate={{
opacity: [0.3, 1, 0.3]
}}
transition={{
duration: 2,
repeat: Infinity,
delay: index * 0.3,
ease: "easeInOut"
}}
className="text-center p-3 bg-white rounded-lg shadow-sm border border-slate-100"
>
<IconComponent className="w-6 h-6 text-slate-400 mx-auto mb-1" />
<span className="text-xs text-slate-500">{item.label}</span>
</motion.div>
)
})}
</motion.div>
</div>
</div>
)
}

View File

@@ -0,0 +1,459 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import {
ArrowLeft,
Package,
User,
CreditCard,
Truck,
CheckCircle,
XCircle,
Clock,
MapPin,
Phone,
Mail
} from 'lucide-react'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { toast } from 'sonner'
import { motion } from 'framer-motion'
import Link from 'next/link'
import Image from 'next/image'
interface OrderItem {
id: string
quantity: number
price: number
product: {
id: string
name: string
images: string[]
sku: string
price: number
}
}
interface ShippingAddress {
firstName: string
lastName: string
company?: string | null
address1: string
address2?: string | null
city: string
state: string
zipCode: string
country: string
phone?: string | null
}
interface Order {
id: string
total: number
status: 'PENDING' | 'PAID' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED'
createdAt: string
updatedAt: string
razorpayOrderId: string | null
razorpayPaymentId: string | null
shippingAddress: ShippingAddress | null
user: {
id: string
name: string
email: string
phone: string | null
}
orderItems: OrderItem[]
}
const statusColors = {
PENDING: 'bg-yellow-100 text-yellow-800 border-yellow-200',
PAID: 'bg-blue-100 text-blue-800 border-blue-200',
SHIPPED: 'bg-purple-100 text-purple-800 border-purple-200',
DELIVERED: 'bg-green-100 text-green-800 border-green-200',
CANCELLED: 'bg-red-100 text-red-800 border-red-200'
}
const statusIcons = {
PENDING: Clock,
PAID: CreditCard,
SHIPPED: Truck,
DELIVERED: CheckCircle,
CANCELLED: XCircle
}
export default function AdminOrderDetail() {
const params = useParams()
const router = useRouter()
const [order, setOrder] = useState<Order | null>(null)
const [loading, setLoading] = useState(true)
const [updating, setUpdating] = useState(false)
const orderId = params.id as string
const fetchOrder = useCallback(async () => {
try {
setLoading(true)
const response = await fetch(`/api/admin/orders/${orderId}`)
if (!response.ok) {
throw new Error('Failed to fetch order')
}
const data = await response.json()
setOrder(data)
} catch (error) {
console.error('Error fetching order:', error)
toast.error('Failed to load order details')
} finally {
setLoading(false)
}
}, [orderId])
const updateOrderStatus = async (newStatus: string) => {
try {
setUpdating(true)
const response = await fetch(`/api/admin/orders/${orderId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ status: newStatus }),
})
if (!response.ok) {
throw new Error('Failed to update order status')
}
const data = await response.json()
setOrder(data)
toast.success('Order status updated successfully')
} catch (error) {
console.error('Error updating order:', error)
toast.error('Failed to update order status')
} finally {
setUpdating(false)
}
}
useEffect(() => {
if (orderId) {
fetchOrder()
}
}, [orderId, fetchOrder])
if (loading) {
return (
<div className="container mx-auto px-4 py-8">
<div className="animate-pulse space-y-6">
<div className="h-8 bg-gray-200 rounded w-1/4"></div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-4">
<div className="h-96 bg-gray-200 rounded"></div>
</div>
<div className="space-y-4">
<div className="h-48 bg-gray-200 rounded"></div>
<div className="h-32 bg-gray-200 rounded"></div>
</div>
</div>
</div>
</div>
)
}
if (!order) {
return (
<div className="container mx-auto px-4 py-8">
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Package className="h-12 w-12 text-gray-400 mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">Order Not Found</h3>
<p className="text-gray-600 mb-4">The order you're looking for doesn't exist.</p>
<Link href="/admin/orders">
<Button>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Orders
</Button>
</Link>
</CardContent>
</Card>
</div>
)
}
const StatusIcon = statusIcons[order.status] || Clock
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="container mx-auto px-4 py-8"
>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<Link href="/admin/orders">
<Button variant="outline" size="sm">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Orders
</Button>
</Link>
<div>
<h1 className="text-2xl font-bold text-gray-900">Order #{order.id}</h1>
<p className="text-gray-600">
Placed on {new Date(order.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</p>
</div>
</div>
<Badge className={statusColors[order.status]}>
<StatusIcon className="h-4 w-4 mr-1" />
{order.status}
</Badge>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Order Items */}
<div className="lg:col-span-2">
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<Package className="h-5 w-5 mr-2" />
Order Items ({order.orderItems?.length || 0})
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{order.orderItems && order.orderItems.length > 0 ? (
order.orderItems.map((item) => (
<div key={item.id} className="flex items-center space-x-4 p-4 border rounded-lg bg-gray-50">
<div className="w-16 h-16 bg-gray-200 rounded-lg flex items-center justify-center">
{item.product?.images && item.product.images.length > 0 ? (
<Image
src={item.product.images[0]}
alt={item.product.name || 'Product'}
width={64}
height={64}
className="w-full h-full object-cover rounded-lg"
/>
) : (
<Package className="h-8 w-8 text-gray-400" />
)}
</div>
<div className="flex-1">
<h4 className="font-semibold text-gray-900">{item.product?.name || 'Unknown Product'}</h4>
<p className="text-sm text-gray-600">SKU: {item.product?.sku || 'N/A'}</p>
<div className="flex items-center space-x-4 mt-1">
<span className="text-sm text-gray-600">Qty: {item.quantity}</span>
<span className="text-sm text-gray-600">Price: {item.price.toFixed(2)} each</span>
</div>
</div>
<div className="text-right">
<p className="font-semibold text-gray-900">
{(item.price * item.quantity).toFixed(2)}
</p>
<p className="text-xs text-gray-500">
{item.quantity} × {item.price.toFixed(2)}
</p>
</div>
</div>
))
) : (
<div className="text-center py-8">
<Package className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">No items found in this order</p>
</div>
)}
</div>
<Separator className="my-4" />
<div className="flex justify-between items-center">
<span className="text-lg font-semibold">Total Amount:</span>
<span className="text-xl font-bold text-green-600">{order.total?.toFixed(2) || '0.00'}</span>
</div>
</CardContent>
</Card>
</div>
{/* Order Details Sidebar */}
<div className="space-y-6">
{/* Order Actions */}
<Card>
<CardHeader>
<CardTitle>Order Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div>
<label className="text-sm font-medium text-gray-700 mb-2 block">
Update Order Status
</label>
<Select
value={order.status}
onValueChange={(value) => updateOrderStatus(value)}
disabled={updating}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="PENDING">
<div className="flex items-center">
<Clock className="h-4 w-4 mr-2 text-yellow-600" />
Pending
</div>
</SelectItem>
<SelectItem value="PAID">
<div className="flex items-center">
<CreditCard className="h-4 w-4 mr-2 text-blue-600" />
Paid
</div>
</SelectItem>
<SelectItem value="SHIPPED">
<div className="flex items-center">
<Truck className="h-4 w-4 mr-2 text-purple-600" />
Shipped
</div>
</SelectItem>
<SelectItem value="DELIVERED">
<div className="flex items-center">
<CheckCircle className="h-4 w-4 mr-2 text-green-600" />
Delivered
</div>
</SelectItem>
<SelectItem value="CANCELLED">
<div className="flex items-center">
<XCircle className="h-4 w-4 mr-2 text-red-600" />
Cancelled
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{updating && (
<div className="text-sm text-gray-500 flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900 mr-2"></div>
Updating order status...
</div>
)}
</CardContent>
</Card>
{/* Customer Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<User className="h-5 w-5 mr-2" />
Customer Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center space-x-3">
<User className="h-4 w-4 text-gray-500" />
<span>{order.user?.name || 'Unknown User'}</span>
</div>
<div className="flex items-center space-x-3">
<Mail className="h-4 w-4 text-gray-500" />
<span className="text-sm">{order.user?.email || 'No email'}</span>
</div>
{order.user?.phone && (
<div className="flex items-center space-x-3">
<Phone className="h-4 w-4 text-gray-500" />
<span>{order.user.phone}</span>
</div>
)}
</CardContent>
</Card>
{/* Shipping Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<MapPin className="h-5 w-5 mr-2" />
Shipping Address
</CardTitle>
</CardHeader>
<CardContent>
{order.shippingAddress ? (
<div className="space-y-2">
<p className="font-semibold">
{order.shippingAddress.firstName} {order.shippingAddress.lastName}
</p>
{order.shippingAddress.company && (
<p className="text-sm text-gray-600">{order.shippingAddress.company}</p>
)}
<p className="text-sm">{order.shippingAddress.address1}</p>
{order.shippingAddress.address2 && (
<p className="text-sm">{order.shippingAddress.address2}</p>
)}
<p className="text-sm">
{order.shippingAddress.city}, {order.shippingAddress.state} {order.shippingAddress.zipCode}
</p>
<p className="text-sm">{order.shippingAddress.country}</p>
{order.shippingAddress.phone && (
<p className="text-sm flex items-center mt-2">
<Phone className="h-4 w-4 mr-1" />
{order.shippingAddress.phone}
</p>
)}
</div>
) : (
<p className="text-sm text-gray-500">No shipping address provided</p>
)}
</CardContent>
</Card>
{/* Payment Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<CreditCard className="h-5 w-5 mr-2" />
Payment Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div>
<span className="text-sm font-medium text-gray-500">Payment Status:</span>
<Badge className={`ml-2 ${statusColors[order.status]}`}>
{order.status}
</Badge>
</div>
{order.razorpayOrderId && (
<div>
<span className="text-sm font-medium text-gray-500">Razorpay Order ID:</span>
<p className="text-sm font-mono bg-gray-50 p-2 rounded mt-1">{order.razorpayOrderId}</p>
</div>
)}
{order.razorpayPaymentId && (
<div>
<span className="text-sm font-medium text-gray-500">Payment ID:</span>
<p className="text-sm font-mono bg-gray-50 p-2 rounded mt-1">{order.razorpayPaymentId}</p>
</div>
)}
<Separator />
<div>
<span className="text-sm font-medium text-gray-500">Total Amount:</span>
<p className="text-lg font-bold text-green-600">{order.total?.toFixed(2) || '0.00'}</p>
</div>
</CardContent>
</Card>
</div>
</div>
</motion.div>
)
}

484
app/admin/orders/page.tsx Normal file
View File

@@ -0,0 +1,484 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Search, Download, MoreHorizontal, Eye, Package, Truck, CheckCircle, XCircle, Clock, CreditCard, Filter } from 'lucide-react'
import { toast } from 'sonner'
import Link from 'next/link'
import { motion } from 'framer-motion'
interface Order {
id: string
total: number
status: 'PENDING' | 'PAID' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED'
createdAt: string
razorpayOrderId: string | null
razorpayPaymentId: string | null
user: {
id: string
name: string
email: string
}
orderItems: {
id: string
quantity: number
price: number
product: {
id: string
name: string
images: string[]
}
}[]
}
const statusConfig = {
PENDING: {
label: 'Pending',
color: 'bg-gradient-to-r from-yellow-400 to-orange-500',
icon: Clock,
description: 'Awaiting payment'
},
PAID: {
label: 'Paid',
color: 'bg-gradient-to-r from-blue-500 to-indigo-600',
icon: CreditCard,
description: 'Payment confirmed'
},
SHIPPED: {
label: 'Shipped',
color: 'bg-gradient-to-r from-purple-500 to-pink-600',
icon: Truck,
description: 'Order shipped'
},
DELIVERED: {
label: 'Delivered',
color: 'bg-gradient-to-r from-green-500 to-emerald-600',
icon: CheckCircle,
description: 'Successfully delivered'
},
CANCELLED: {
label: 'Cancelled',
color: 'bg-gradient-to-r from-red-500 to-pink-600',
icon: XCircle,
description: 'Order cancelled'
}
}
export default function AdminOrdersPage() {
const [orders, setOrders] = useState<Order[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState('')
const [statusFilter, setStatusFilter] = useState('all')
const [selectedOrders, setSelectedOrders] = useState<string[]>([])
const [bulkActionLoading, setBulkActionLoading] = useState(false)
const fetchOrders = useCallback(async () => {
try {
setLoading(true)
const params = new URLSearchParams()
if (search) params.append('search', search)
if (statusFilter && statusFilter !== 'all') params.append('status', statusFilter)
const response = await fetch(`/api/admin/orders?${params}`)
const data = await response.json()
setOrders(data || [])
} catch (error) {
console.error('Error fetching orders:', error)
toast.error('Failed to load orders')
} finally {
setLoading(false)
}
}, [search, statusFilter])
useEffect(() => {
fetchOrders()
}, [fetchOrders])
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedOrders(orders.map(o => o.id))
} else {
setSelectedOrders([])
}
}
const handleSelectOrder = (orderId: string, checked: boolean) => {
if (checked) {
setSelectedOrders(prev => [...prev, orderId])
} else {
setSelectedOrders(prev => prev.filter(id => id !== orderId))
}
}
const handleBulkStatusUpdate = async (newStatus: string) => {
if (selectedOrders.length === 0) {
toast.error('Please select at least one order')
return
}
setBulkActionLoading(true)
try {
const response = await fetch('/api/admin/orders/bulk', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orderIds: selectedOrders,
status: newStatus
})
})
if (!response.ok) throw new Error('Bulk update failed')
toast.success(`Successfully updated ${selectedOrders.length} orders`)
setSelectedOrders([])
fetchOrders()
} catch (error) {
toast.error('Failed to update orders')
} finally {
setBulkActionLoading(false)
}
}
const handleStatusUpdate = async (orderId: string, newStatus: string) => {
try {
const response = await fetch(`/api/admin/orders/${orderId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus })
})
if (!response.ok) throw new Error('Status update failed')
toast.success('Order status updated successfully')
fetchOrders()
} catch (error) {
toast.error('Failed to update order status')
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100 p-6">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6"
>
<div>
<h1 className="text-4xl font-bold bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text text-transparent">
Orders Management
</h1>
<p className="text-gray-600 mt-2">Manage customer orders and track shipments</p>
</div>
<div className="flex items-center space-x-3">
<Button variant="outline" size="sm" className="bg-white shadow-md hover:shadow-lg transition-shadow">
<Download className="h-4 w-4 mr-2" />
Export Data
</Button>
<Button className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 shadow-lg">
<Filter className="h-4 w-4 mr-2" />
Advanced Filters
</Button>
</div>
</motion.div>
{/* Stats Cards */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6"
>
{Object.entries(statusConfig).map(([status, config], index) => {
const StatusIcon = config.icon
const count = orders.filter(order => order.status === status).length
return (
<Card key={status} className="bg-white/80 backdrop-blur-sm border-0 shadow-lg hover:shadow-xl transition-all duration-300">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">{config.label}</p>
<p className="text-2xl font-bold text-gray-900">{count}</p>
</div>
<div className={`w-12 h-12 ${config.color} rounded-xl flex items-center justify-center shadow-lg`}>
<StatusIcon className="h-6 w-6 text-white" />
</div>
</div>
<p className="text-xs text-gray-500 mt-2">{config.description}</p>
</CardContent>
</Card>
)
})}
</motion.div>
{/* Main Orders Table */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<Card className="bg-white/90 backdrop-blur-sm border-0 shadow-xl">
<CardHeader className="pb-4">
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
<CardTitle className="text-2xl font-bold">All Orders</CardTitle>
<div className="flex flex-wrap items-center gap-3">
{selectedOrders.length > 0 && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="flex items-center space-x-3 bg-blue-50 px-4 py-2 rounded-lg border border-blue-200"
>
<span className="text-sm font-medium text-blue-700">
{selectedOrders.length} selected
</span>
<Select onValueChange={handleBulkStatusUpdate}>
<SelectTrigger className="w-40 bg-white">
<SelectValue placeholder="Update Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="PAID">Mark as Paid</SelectItem>
<SelectItem value="SHIPPED">Mark as Shipped</SelectItem>
<SelectItem value="DELIVERED">Mark as Delivered</SelectItem>
<SelectItem value="CANCELLED">Mark as Cancelled</SelectItem>
</SelectContent>
</Select>
</motion.div>
)}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search orders, customers..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 w-64 bg-white shadow-sm"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-40 bg-white shadow-sm">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="PENDING">Pending</SelectItem>
<SelectItem value="PAID">Paid</SelectItem>
<SelectItem value="SHIPPED">Shipped</SelectItem>
<SelectItem value="DELIVERED">Delivered</SelectItem>
<SelectItem value="CANCELLED">Cancelled</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="relative">
<div className="w-16 h-16 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
<div className="w-10 h-10 border-4 border-gray-100 border-t-purple-500 rounded-full animate-spin absolute top-1 left-1"></div>
</div>
</div>
) : (
<div className="overflow-hidden rounded-lg border border-gray-200">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={selectedOrders.length === orders.length && orders.length > 0}
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead className="font-semibold">Order</TableHead>
<TableHead className="font-semibold">Customer</TableHead>
<TableHead className="font-semibold">Items</TableHead>
<TableHead className="font-semibold">Total</TableHead>
<TableHead className="font-semibold">Status</TableHead>
<TableHead className="font-semibold">Date</TableHead>
<TableHead className="font-semibold">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{orders.map((order, index) => {
const StatusIcon = statusConfig[order.status].icon
return (
<motion.tr
key={order.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className="hover:bg-gray-50 transition-colors"
>
<TableCell>
<Checkbox
checked={selectedOrders.includes(order.id)}
onCheckedChange={(checked) => handleSelectOrder(order.id, !!checked)}
/>
</TableCell>
<TableCell>
<div className="space-y-1">
<p className="font-semibold text-gray-900">#{order.id.slice(-8)}</p>
{order.razorpayOrderId && (
<p className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
Razorpay: {order.razorpayOrderId.slice(-8)}
</p>
)}
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-r from-blue-500 to-purple-600 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-medium">
{order.user.name?.charAt(0) || 'U'}
</span>
</div>
<div>
<p className="font-medium text-gray-900">{order.user.name}</p>
<p className="text-sm text-gray-500">{order.user.email}</p>
</div>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
{order.orderItems.slice(0, 2).map((item) => (
<div key={item.id} className="text-sm">
<span className="font-medium">{item.quantity}x</span>{' '}
<span className="text-gray-600">{item.product.name}</span>
</div>
))}
{order.orderItems.length > 2 && (
<p className="text-xs text-blue-600 font-medium">
+{order.orderItems.length - 2} more items
</p>
)}
</div>
</TableCell>
<TableCell>
<span className="font-bold text-lg bg-gradient-to-r from-green-600 to-emerald-600 bg-clip-text text-transparent">
{order.total.toFixed(2)}
</span>
</TableCell>
<TableCell>
<Badge className={`${statusConfig[order.status].color} text-white shadow-md`}>
<StatusIcon className="h-3 w-3 mr-1" />
{statusConfig[order.status].label}
</Badge>
</TableCell>
<TableCell>
<div className="text-sm">
<p className="font-medium text-gray-900">
{new Date(order.createdAt).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
})}
</p>
<p className="text-gray-500">
{new Date(order.createdAt).toLocaleDateString('en-US', {
year: 'numeric'
})}
</p>
</div>
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="hover:bg-gray-100">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem asChild>
<Link href={`/admin/orders/${order.id}`} className="flex items-center">
<Eye className="h-4 w-4 mr-2" />
View Details
</Link>
</DropdownMenuItem>
{order.status === 'PENDING' && (
<DropdownMenuItem
onClick={() => handleStatusUpdate(order.id, 'PAID')}
className="text-blue-600"
>
<CreditCard className="h-4 w-4 mr-2" />
Mark as Paid
</DropdownMenuItem>
)}
{order.status === 'PAID' && (
<DropdownMenuItem
onClick={() => handleStatusUpdate(order.id, 'SHIPPED')}
className="text-purple-600"
>
<Truck className="h-4 w-4 mr-2" />
Mark as Shipped
</DropdownMenuItem>
)}
{order.status === 'SHIPPED' && (
<DropdownMenuItem
onClick={() => handleStatusUpdate(order.id, 'DELIVERED')}
className="text-green-600"
>
<Package className="h-4 w-4 mr-2" />
Mark as Delivered
</DropdownMenuItem>
)}
{['PENDING', 'PAID'].includes(order.status) && (
<DropdownMenuItem
onClick={() => handleStatusUpdate(order.id, 'CANCELLED')}
className="text-red-600"
>
<XCircle className="h-4 w-4 mr-2" />
Cancel Order
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</motion.tr>
)
})}
</TableBody>
</Table>
</div>
)}
{!loading && orders.length === 0 && (
<div className="text-center py-12">
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Package className="h-10 w-10 text-gray-400" />
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">No orders found</h3>
<p className="text-gray-500">Try adjusting your search or filter criteria</p>
</div>
)}
</CardContent>
</Card>
</motion.div>
</div>
</div>
)
}

253
app/admin/page.tsx Normal file
View File

@@ -0,0 +1,253 @@
'use client'
import { useEffect, useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Users, Package, ShoppingCart, DollarSign, TrendingUp, TrendingDown } from 'lucide-react'
import { motion } from 'framer-motion'
import { Badge } from '@/components/ui/badge'
interface DashboardStats {
totalUsers: number
totalProducts: number
totalOrders: number
totalRevenue: number
userGrowth: number
productGrowth: number
orderGrowth: number
revenueGrowth: number
}
interface RecentOrder {
id: string
total: number
status: string
createdAt: string
user: {
name: string
email: string
}
}
interface TopProduct {
id: string
name: string
price: number
orderCount: number
totalRevenue: number
}
export default function AdminDashboard() {
const [stats, setStats] = useState<DashboardStats | null>(null)
const [recentOrders, setRecentOrders] = useState<RecentOrder[]>([])
const [topProducts, setTopProducts] = useState<TopProduct[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchDashboardData()
}, [])
const fetchDashboardData = async () => {
try {
const [statsRes, ordersRes, productsRes] = await Promise.all([
fetch('/api/admin/dashboard/stats'),
fetch('/api/admin/dashboard/recent-orders'),
fetch('/api/admin/dashboard/top-products')
])
const [statsData, ordersData, productsData] = await Promise.all([
statsRes.json(),
ordersRes.json(),
productsRes.json()
])
setStats(statsData)
setRecentOrders(ordersData.orders || [])
setTopProducts(productsData.products || [])
} catch (error) {
console.error('Error fetching dashboard data:', error)
} finally {
setLoading(false)
}
}
const getStatusColor = (status: string) => {
switch (status) {
case 'PENDING': return 'bg-yellow-100 text-yellow-800'
case 'PAID': return 'bg-blue-100 text-blue-800'
case 'SHIPPED': return 'bg-purple-100 text-purple-800'
case 'DELIVERED': return 'bg-green-100 text-green-800'
case 'CANCELLED': return 'bg-red-100 text-red-800'
default: return 'bg-gray-100 text-gray-800'
}
}
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
<div className="w-16 h-16 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
</div>
)
}
const dashboardStats = stats ? [
{
title: 'Total Users',
value: stats.totalUsers.toLocaleString(),
change: `${stats.userGrowth >= 0 ? '+' : ''}${stats.userGrowth.toFixed(1)}%`,
trend: stats.userGrowth >= 0 ? 'up' : 'down',
icon: Users,
},
{
title: 'Products',
value: stats.totalProducts.toLocaleString(),
change: `${stats.productGrowth >= 0 ? '+' : ''}${stats.productGrowth.toFixed(1)}%`,
trend: stats.productGrowth >= 0 ? 'up' : 'down',
icon: Package,
},
{
title: 'Orders',
value: stats.totalOrders.toLocaleString(),
change: `${stats.orderGrowth >= 0 ? '+' : ''}${stats.orderGrowth.toFixed(1)}%`,
trend: stats.orderGrowth >= 0 ? 'up' : 'down',
icon: ShoppingCart,
},
{
title: 'Revenue',
value: `${stats.totalRevenue.toLocaleString()}`,
change: `${stats.revenueGrowth >= 0 ? '+' : ''}${stats.revenueGrowth.toFixed(1)}%`,
trend: stats.revenueGrowth >= 0 ? 'up' : 'down',
icon: DollarSign,
},
] : []
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
<div>
<h1 className="text-3xl font-bold text-gray-900">Admin Dashboard</h1>
<p className="text-gray-600 mt-2">Welcome to your admin dashboard</p>
</div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{dashboardStats.map((stat, index) => (
<motion.div
key={stat.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
>
<Card className="bg-white/80 backdrop-blur-sm shadow-lg border-0">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-600">
{stat.title}
</CardTitle>
<stat.icon className="h-5 w-5 text-gray-400" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-900">{stat.value}</div>
<p className="text-xs text-muted-foreground flex items-center mt-2">
{stat.trend === 'up' ? (
<TrendingUp className="h-3 w-3 text-green-500 mr-1" />
) : (
<TrendingDown className="h-3 w-3 text-red-500 mr-1" />
)}
<span className={stat.trend === 'up' ? 'text-green-600 font-medium' : 'text-red-600 font-medium'}>
{stat.change}
</span>
<span className="ml-1 text-gray-500">from last month</span>
</p>
</CardContent>
</Card>
</motion.div>
))}
</div>
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
{/* Recent Orders */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4 }}
>
<Card className="bg-white/80 backdrop-blur-sm shadow-lg border-0">
<CardHeader>
<CardTitle>Recent Orders</CardTitle>
<CardDescription>Latest orders from your store</CardDescription>
</CardHeader>
<CardContent>
{recentOrders.length === 0 ? (
<div className="text-center py-8">
<ShoppingCart className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">No recent orders</p>
</div>
) : (
<div className="space-y-4">
{recentOrders.map((order) => (
<div key={order.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<p className="font-medium">Order #{order.id.slice(-8)}</p>
<Badge className={getStatusColor(order.status)}>
{order.status}
</Badge>
</div>
<p className="text-sm text-gray-500">{order.user.name}</p>
<p className="text-xs text-gray-400">
{new Date(order.createdAt).toLocaleDateString()}
</p>
</div>
<span className="font-bold text-green-600">{order.total.toFixed(2)}</span>
</div>
))}
</div>
)}
</CardContent>
</Card>
</motion.div>
{/* Top Products */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
>
<Card className="bg-white/80 backdrop-blur-sm shadow-lg border-0">
<CardHeader>
<CardTitle>Top Products</CardTitle>
<CardDescription>Best selling products this month</CardDescription>
</CardHeader>
<CardContent>
{topProducts.length === 0 ? (
<div className="text-center py-8">
<Package className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">No product data available</p>
</div>
) : (
<div className="space-y-4">
{topProducts.map((product, index) => (
<div key={product.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<span className="text-sm font-bold text-blue-600">#{index + 1}</span>
</div>
<div>
<p className="font-medium text-gray-900">{product.name}</p>
<p className="text-sm text-gray-500">{product.orderCount} sold</p>
</div>
</div>
<div className="text-right">
<p className="font-bold text-gray-900">{product.price.toFixed(2)}</p>
<p className="text-xs text-green-600">{product.totalRevenue.toFixed(0)} revenue</p>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</motion.div>
</div>
</div>
</div>
)
}

408
app/admin/payouts/page.tsx Normal file
View File

@@ -0,0 +1,408 @@
'use client'
import { useEffect, useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { DollarSign, Clock, CheckCircle, XCircle, Eye, Search, Filter } from 'lucide-react'
import { motion } from 'framer-motion'
import { toast } from 'sonner'
interface Payout {
id: string
amount: number
status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'PAID'
bankDetails: string
adminNotes?: string
createdAt: string
updatedAt: string
user: {
id: string
name: string
email: string
}
}
interface PayoutStats {
totalRequests: number
pendingAmount: number
approvedAmount: number
paidAmount: number
}
export default function AdminPayoutsPage() {
const [payouts, setPayouts] = useState<Payout[]>([])
const [stats, setStats] = useState<PayoutStats | null>(null)
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState({
status: 'all',
search: ''
})
const [selectedPayout, setSelectedPayout] = useState<Payout | null>(null)
const [reviewDialogOpen, setReviewDialogOpen] = useState(false)
const [reviewStatus, setReviewStatus] = useState<'APPROVED' | 'REJECTED'>('APPROVED')
const [adminNotes, setAdminNotes] = useState('')
const [processing, setProcessing] = useState(false)
useEffect(() => {
fetchPayouts()
fetchStats()
}, [])
const fetchPayouts = async () => {
try {
const response = await fetch('/api/admin/payouts')
if (!response.ok) throw new Error('Failed to fetch payouts')
const data = await response.json()
setPayouts(data.payouts || [])
} catch (error) {
console.error('Error fetching payouts:', error)
toast.error('Failed to load payouts')
} finally {
setLoading(false)
}
}
const fetchStats = async () => {
try {
const response = await fetch('/api/admin/payouts/stats')
if (!response.ok) throw new Error('Failed to fetch stats')
const data = await response.json()
setStats(data)
} catch (error) {
console.error('Error fetching stats:', error)
}
}
const handleReviewPayout = async () => {
if (!selectedPayout) return
setProcessing(true)
try {
const response = await fetch(`/api/admin/payouts/${selectedPayout.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
status: reviewStatus,
adminNotes
})
})
if (!response.ok) throw new Error('Failed to update payout')
toast.success(`Payout ${reviewStatus.toLowerCase()} successfully`)
setReviewDialogOpen(false)
setSelectedPayout(null)
setAdminNotes('')
fetchPayouts()
fetchStats()
} catch (error) {
console.error('Error updating payout:', error)
toast.error('Failed to update payout')
} finally {
setProcessing(false)
}
}
const openReviewDialog = (payout: Payout) => {
setSelectedPayout(payout)
setAdminNotes(payout.adminNotes || '')
setReviewDialogOpen(true)
}
const getStatusColor = (status: string) => {
switch (status) {
case 'PENDING': return 'bg-yellow-100 text-yellow-800'
case 'APPROVED': return 'bg-blue-100 text-blue-800'
case 'REJECTED': return 'bg-red-100 text-red-800'
case 'PAID': return 'bg-green-100 text-green-800'
default: return 'bg-gray-100 text-gray-800'
}
}
const getStatusIcon = (status: string) => {
switch (status) {
case 'PENDING': return <Clock className="h-4 w-4 text-yellow-500" />
case 'APPROVED': return <CheckCircle className="h-4 w-4 text-blue-500" />
case 'REJECTED': return <XCircle className="h-4 w-4 text-red-500" />
case 'PAID': return <CheckCircle className="h-4 w-4 text-green-500" />
default: return <Clock className="h-4 w-4 text-gray-500" />
}
}
const filteredPayouts = payouts.filter(payout => {
const matchesStatus = filter.status === 'all' || payout.status === filter.status
const matchesSearch = !filter.search ||
payout.user.name.toLowerCase().includes(filter.search.toLowerCase()) ||
payout.user.email.toLowerCase().includes(filter.search.toLowerCase())
return matchesStatus && matchesSearch
})
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
<div className="w-16 h-16 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Payout Management</h1>
<p className="text-gray-600 mt-2">Review and manage user payout requests</p>
</div>
</div>
{/* Stats Cards */}
{stats && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card className="bg-gradient-to-r from-blue-500 to-blue-600 text-white">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100 text-sm">Total Requests</p>
<p className="text-2xl font-bold">{stats.totalRequests}</p>
</div>
<DollarSign className="h-8 w-8 text-blue-200" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-yellow-500 to-yellow-600 text-white">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-yellow-100 text-sm">Pending Amount</p>
<p className="text-2xl font-bold">{stats.pendingAmount.toFixed(0)}</p>
</div>
<Clock className="h-8 w-8 text-yellow-200" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-purple-500 to-purple-600 text-white">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-purple-100 text-sm">Approved Amount</p>
<p className="text-2xl font-bold">{stats.approvedAmount.toFixed(0)}</p>
</div>
<CheckCircle className="h-8 w-8 text-purple-200" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-green-500 to-green-600 text-white">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-green-100 text-sm">Paid Amount</p>
<p className="text-2xl font-bold">{stats.paidAmount.toFixed(0)}</p>
</div>
<CheckCircle className="h-8 w-8 text-green-200" />
</div>
</CardContent>
</Card>
</div>
)}
{/* Filters */}
<Card>
<CardHeader>
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<CardTitle>Payout Requests</CardTitle>
<div className="flex items-center space-x-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search users..."
value={filter.search}
onChange={(e) => setFilter(prev => ({ ...prev, search: e.target.value }))}
className="pl-10 w-64"
/>
</div>
<Select value={filter.status} onValueChange={(value) => setFilter(prev => ({ ...prev, status: value }))}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="PENDING">Pending</SelectItem>
<SelectItem value="APPROVED">Approved</SelectItem>
<SelectItem value="REJECTED">Rejected</SelectItem>
<SelectItem value="PAID">Paid</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
{filteredPayouts.length === 0 ? (
<div className="text-center py-8">
<DollarSign className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No payout requests found</h3>
<p className="text-gray-500">Payout requests will appear here when users submit them</p>
</div>
) : (
<div className="space-y-4">
{filteredPayouts.map((payout, index) => (
<motion.div
key={payout.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className="flex items-center justify-between p-4 border border-gray-200 rounded-lg hover:shadow-md transition-shadow"
>
<div className="flex items-center space-x-4">
{getStatusIcon(payout.status)}
<div>
<div className="flex items-center space-x-2 mb-1">
<span className="font-medium text-gray-900">{payout.user.name}</span>
<Badge className={getStatusColor(payout.status)}>
{payout.status}
</Badge>
</div>
<p className="text-sm text-gray-500">{payout.user.email}</p>
<p className="text-xs text-gray-400">
Requested {new Date(payout.createdAt).toLocaleDateString()}
</p>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="text-right">
<p className="text-lg font-bold text-gray-900">{payout.amount.toFixed(2)}</p>
<p className="text-xs text-gray-500">
Updated {new Date(payout.updatedAt).toLocaleDateString()}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => openReviewDialog(payout)}
disabled={payout.status === 'PAID'}
>
<Eye className="h-4 w-4 mr-1" />
{payout.status === 'PENDING' ? 'Review' : 'View'}
</Button>
</div>
</motion.div>
))}
</div>
)}
</CardContent>
</Card>
{/* Review Dialog */}
<Dialog open={reviewDialogOpen} onOpenChange={setReviewDialogOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Review Payout Request</DialogTitle>
<DialogDescription>
Review and take action on this payout request
</DialogDescription>
</DialogHeader>
{selectedPayout && (
<div className="space-y-6">
{/* User Info */}
<div className="p-4 bg-gray-50 rounded-lg">
<h4 className="font-medium text-gray-900 mb-2">User Information</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-500">Name:</span>
<p className="font-medium">{selectedPayout.user.name}</p>
</div>
<div>
<span className="text-gray-500">Email:</span>
<p className="font-medium">{selectedPayout.user.email}</p>
</div>
<div>
<span className="text-gray-500">Amount:</span>
<p className="font-bold text-lg">{selectedPayout.amount.toFixed(2)}</p>
</div>
<div>
<span className="text-gray-500">Status:</span>
<Badge className={getStatusColor(selectedPayout.status)}>
{selectedPayout.status}
</Badge>
</div>
</div>
</div>
{/* Bank Details */}
<div>
<Label>Bank Details</Label>
<div className="p-3 bg-gray-50 rounded border mt-1">
<pre className="text-sm whitespace-pre-wrap">{selectedPayout.bankDetails}</pre>
</div>
</div>
{/* Action Section */}
{selectedPayout.status === 'PENDING' && (
<div className="space-y-4">
<div>
<Label>Action</Label>
<Select value={reviewStatus} onValueChange={(value: 'APPROVED' | 'REJECTED') => setReviewStatus(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="APPROVED">Approve</SelectItem>
<SelectItem value="REJECTED">Reject</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Admin Notes</Label>
<Textarea
value={adminNotes}
onChange={(e) => setAdminNotes(e.target.value)}
placeholder="Add notes about this decision..."
rows={3}
/>
</div>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={() => setReviewDialogOpen(false)}>
Cancel
</Button>
<Button onClick={handleReviewPayout} disabled={processing}>
{processing ? 'Processing...' : `${reviewStatus === 'APPROVED' ? 'Approve' : 'Reject'} Payout`}
</Button>
</div>
</div>
)}
{/* Existing Notes */}
{selectedPayout.adminNotes && selectedPayout.status !== 'PENDING' && (
<div>
<Label>Admin Notes</Label>
<div className="p-3 bg-gray-50 rounded border mt-1">
<p className="text-sm">{selectedPayout.adminNotes}</p>
</div>
</div>
)}
</div>
)}
</DialogContent>
</Dialog>
</div>
</div>
)
}

View File

@@ -0,0 +1,84 @@
'use client'
import { useState, useEffect } from 'react'
import { useParams } from 'next/navigation'
import { ProductForm } from '@/components/admin/ProductForm'
import { toast } from 'sonner'
interface Product {
id: string
name: string
description: string
price: number
discount: number
images: string[]
stock: number
manageStock: boolean
sku: string
isActive: boolean
categoryId: string
category: {
id: string
name: string
}
}
export default function EditProductPage() {
const params = useParams()
const [product, setProduct] = useState<Product | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
if (params.id) {
fetchProduct(params.id as string)
}
}, [params.id])
const fetchProduct = async (id: string) => {
try {
const response = await fetch(`/api/admin/products/${id}`)
if (!response.ok) throw new Error('Product not found')
const data = await response.json()
// Transform the data to match the form's expected structure
const transformedProduct = {
...data,
categoryId: data.category.id
}
setProduct(transformedProduct)
} catch (error) {
console.error('Error fetching product:', error)
toast.error('Failed to load product')
} finally {
setLoading(false)
}
}
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div>
</div>
)
}
if (!product) {
return (
<div className="text-center py-12">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Product not found</h2>
<p className="text-gray-600">The product you&apos;re looking for doesn&apos;t exist.</p>
</div>
)
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Edit Product</h1>
<p className="text-gray-600">Update product information</p>
</div>
<ProductForm product={product} />
</div>
)
}

View File

@@ -0,0 +1,14 @@
import { ProductForm } from '@/components/admin/ProductForm'
export default function NewProductPage() {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Add New Product</h1>
<p className="text-gray-600">Create a new product in your catalog</p>
</div>
<ProductForm />
</div>
)
}

653
app/admin/products/page.tsx Normal file
View File

@@ -0,0 +1,653 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import CsvImport from '@/components/ui/csv-import'
import CsvExport from '@/components/ui/csv-export'
import { Search, Plus, Edit, Trash2, MoreHorizontal, Download, Save, Upload } from 'lucide-react'
import { toast } from 'sonner'
import Link from 'next/link'
import Image from 'next/image'
interface Product {
id: string
name: string
description: string | null
price: number
discount: number
stock: number
isActive: boolean
images: string[]
sku: string
weight: string | null
categoryId: string
category: {
id: string
name: string
}
}
interface QuickEditFormData {
name: string
description: string
price: number
discount: number
stock: number
weight: string
}
export default function AdminProductsPage() {
const [products, setProducts] = useState<Product[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState('')
const [selectedProducts, setSelectedProducts] = useState<string[]>([])
const [bulkActionLoading, setBulkActionLoading] = useState(false)
const [quickEditOpen, setQuickEditOpen] = useState(false)
const [editingProduct, setEditingProduct] = useState<Product | null>(null)
const [quickEditForm, setQuickEditForm] = useState<QuickEditFormData>({
name: '',
description: '',
price: 0,
discount: 0,
stock: 0,
weight: ''
})
const [quickEditLoading, setQuickEditLoading] = useState(false)
const fetchProducts = useCallback(async () => {
try {
setLoading(true)
const params = new URLSearchParams({
page: '1',
limit: '20',
admin: 'true' // Add admin flag to get all products
})
if (search) params.append('search', search)
const response = await fetch(`/api/products?${params}`)
const data = await response.json()
setProducts(data.products || [])
} catch (error) {
console.error('Error fetching products:', error)
toast.error('Failed to load products')
} finally {
setLoading(false)
}
}, [search])
useEffect(() => {
fetchProducts()
}, [fetchProducts])
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedProducts(products.map(p => p.id))
} else {
setSelectedProducts([])
}
}
const handleSelectProduct = (productId: string, checked: boolean) => {
if (checked) {
setSelectedProducts(prev => [...prev, productId])
} else {
setSelectedProducts(prev => prev.filter(id => id !== productId))
}
}
const handleBulkAction = async (action: 'activate' | 'deactivate' | 'delete') => {
if (selectedProducts.length === 0) {
toast.error('Please select at least one product')
return
}
setBulkActionLoading(true)
try {
const response = await fetch('/api/admin/products/bulk', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productIds: selectedProducts,
action
})
})
if (!response.ok) throw new Error('Bulk action failed')
toast.success(`Successfully ${action}d ${selectedProducts.length} products`)
setSelectedProducts([])
fetchProducts()
} catch (error) {
toast.error(`Failed to ${action} products`)
} finally {
setBulkActionLoading(false)
}
}
const handleDeleteProduct = async (productId: string) => {
if (!confirm('Are you sure you want to delete this product?')) return
try {
const response = await fetch(`/api/admin/products/${productId}`, {
method: 'DELETE'
})
if (!response.ok) throw new Error('Delete failed')
toast.success('Product deleted successfully')
fetchProducts()
} catch (error) {
toast.error('Failed to delete product')
}
}
const handleQuickEdit = (product: Product) => {
setEditingProduct(product)
setQuickEditForm({
name: product.name,
description: product.description || '',
price: product.price,
discount: product.discount,
stock: product.stock,
weight: product.weight || ''
})
setQuickEditOpen(true)
}
const handleQuickEditSave = async () => {
if (!editingProduct) return
setQuickEditLoading(true)
try {
const response = await fetch(`/api/admin/products/${editingProduct.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: quickEditForm.name,
description: quickEditForm.description,
price: quickEditForm.price,
discount: quickEditForm.discount,
stock: quickEditForm.stock,
weight: quickEditForm.weight,
// Keep existing values that aren't being edited
images: editingProduct.images,
sku: editingProduct.sku,
isActive: editingProduct.isActive,
categoryId: editingProduct.categoryId || editingProduct.category.id
})
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Update failed')
}
toast.success('Product updated successfully')
setQuickEditOpen(false)
setEditingProduct(null)
fetchProducts()
} catch (error) {
console.error('Quick edit error:', error)
toast.error(error instanceof Error ? error.message : 'Failed to update product')
} finally {
setQuickEditLoading(false)
}
}
const handleQuickEditInputChange = (field: keyof QuickEditFormData, value: string | number) => {
setQuickEditForm(prev => ({
...prev,
[field]: value
}))
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Products</h1>
<p className="text-gray-600">Manage your product catalog</p>
</div>
<div className="flex items-center space-x-2">
<CsvExport
title="Export Products"
description="Export products to a CSV file. Select the columns you want to include."
columns={[
{ key: 'name', label: 'Name' },
{ key: 'description', label: 'Description' },
{ key: 'price', label: 'Price' },
{ key: 'category.name', label: 'Category' },
{ key: 'stock', label: 'Stock' },
{ key: 'active', label: 'Status' },
{ key: 'createdAt', label: 'Created At' }
]}
onExport={async (selectedColumns, filters, onProgress) => {
try {
const response = await fetch('/api/admin/products/export', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
columns: selectedColumns,
filters
}),
})
if (!response.ok) {
throw new Error('Export failed')
}
const result = await response.json()
return {
success: true,
data: result.data || products,
message: 'Export completed successfully'
}
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Export failed',
}
}
}}
>
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
Export
</Button>
</CsvExport>
<CsvImport
title="Import Products"
description="Import products from a CSV file. Download the template to see the required format."
templateColumns={[
{ key: 'name', label: 'Name', required: true },
{ key: 'description', label: 'Description', required: true },
{ key: 'price', label: 'Price', required: true },
{ key: 'categoryId', label: 'Category ID', required: true },
{ key: 'stock', label: 'Stock', required: false },
{ key: 'active', label: 'Active', required: false }
]}
onImport={async (data, onProgress) => {
try {
const response = await fetch('/api/admin/products/import', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ data }),
})
if (!response.ok) {
throw new Error('Import failed')
}
const result = await response.json()
fetchProducts() // Refresh the products list
return result
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Import failed',
}
}
}}
>
<Button variant="outline" size="sm">
<Upload className="h-4 w-4 mr-2" />
Import
</Button>
</CsvImport>
<Button asChild>
<Link href="/admin/products/new">
<Plus className="h-4 w-4 mr-2" />
Add Product
</Link>
</Button>
</div>
</div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>All Products</CardTitle>
<div className="flex items-center space-x-2">
{selectedProducts.length > 0 && (
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-500">
{selectedProducts.length} selected
</span>
<Button
variant="outline"
size="sm"
onClick={() => handleBulkAction('activate')}
disabled={bulkActionLoading}
>
Activate
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleBulkAction('deactivate')}
disabled={bulkActionLoading}
>
Deactivate
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleBulkAction('delete')}
disabled={bulkActionLoading}
className="text-red-600"
>
Delete
</Button>
</div>
)}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search products..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 w-64"
/>
</div>
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
Export
</Button>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
{loading ? (
<div className="text-center py-8">Loading...</div>
) : (
<div className="border rounded-lg">
<Table>
<TableHeader className="sticky top-0 bg-white z-10">
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={selectedProducts.length === products.length && products.length > 0}
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>Product</TableHead>
<TableHead>SKU</TableHead>
<TableHead>Category</TableHead>
<TableHead>Price</TableHead>
<TableHead>Stock</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
</Table>
<div className="max-h-[55vh] overflow-y-auto">
<Table>
<TableBody>
{products.map((product) => (
<TableRow key={product.id}>
<TableCell className="w-12">
<Checkbox
checked={selectedProducts.includes(product.id)}
onCheckedChange={(checked) => handleSelectProduct(product.id, !!checked)}
/>
</TableCell>
<TableCell>
<div className="flex items-center space-x-3">
<Image
src={product.images[0] || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
alt={product.name}
width={40}
height={40}
className="rounded-lg object-cover"
/>
<div>
<p className="font-medium">{product.name}</p>
{product.discount > 0 && (
<Badge variant="secondary" className="text-xs">
{product.discount}% OFF
</Badge>
)}
</div>
</div>
</TableCell>
<TableCell className="font-mono text-sm">{product.sku}</TableCell>
<TableCell>{product.category.name}</TableCell>
<TableCell>
<div>
{product.discount > 0 ? (
<>
<span className="font-bold text-green-600">
{(product.price - (product.price * product.discount / 100)).toFixed(2)}
</span>
<span className="text-sm text-gray-500 line-through ml-2">
{product.price.toFixed(2)}
</span>
</>
) : (
<span className="font-bold">{product.price.toFixed(2)}</span>
)}
</div>
</TableCell>
<TableCell>
<Badge variant={product.stock > 10 ? 'default' : product.stock > 0 ? 'secondary' : 'destructive'}>
{product.stock}
</Badge>
</TableCell>
<TableCell>
<Badge variant={product.isActive ? 'default' : 'secondary'}>
{product.isActive ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handleQuickEdit(product)}
className="h-8 px-2"
>
<Edit className="h-3 w-3 mr-1" />
Quick Edit
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<Link href={`/admin/products/${product.id}/edit`}>
<Edit className="h-4 w-4 mr-2" />
Full Edit
</Link>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteProduct(product.id)}
className="text-red-600"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
</CardContent>
</Card>
{/* Quick Edit Dialog */}
<Dialog open={quickEditOpen} onOpenChange={setQuickEditOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Quick Edit Product</DialogTitle>
</DialogHeader>
{editingProduct && (
<div className="space-y-6">
{/* Product Image and Basic Info */}
<div className="flex items-start space-x-4">
<Image
src={editingProduct.images[0] || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
alt={editingProduct.name}
width={80}
height={80}
className="rounded-lg object-cover"
/>
<div className="flex-1">
<p className="text-sm text-gray-500">SKU: {editingProduct.sku}</p>
<p className="text-sm text-gray-500">Category: {editingProduct.category.name}</p>
</div>
</div>
{/* Form Fields */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="quick-name">Product Name</Label>
<Input
id="quick-name"
value={quickEditForm.name}
onChange={(e) => handleQuickEditInputChange('name', e.target.value)}
placeholder="Product name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="quick-weight">Weight</Label>
<Input
id="quick-weight"
value={quickEditForm.weight}
onChange={(e) => handleQuickEditInputChange('weight', e.target.value)}
placeholder="e.g., 1kg, 500g"
/>
</div>
<div className="space-y-2">
<Label htmlFor="quick-price">Price ()</Label>
<Input
id="quick-price"
type="number"
step="0.01"
value={quickEditForm.price}
onChange={(e) => handleQuickEditInputChange('price', parseFloat(e.target.value) || 0)}
placeholder="0.00"
/>
</div>
<div className="space-y-2">
<Label htmlFor="quick-discount">Discount (%)</Label>
<Input
id="quick-discount"
type="number"
min="0"
max="100"
value={quickEditForm.discount}
onChange={(e) => handleQuickEditInputChange('discount', parseFloat(e.target.value) || 0)}
placeholder="0"
/>
</div>
<div className="space-y-2">
<Label htmlFor="quick-stock">Stock Quantity</Label>
<Input
id="quick-stock"
type="number"
min="0"
value={quickEditForm.stock}
onChange={(e) => handleQuickEditInputChange('stock', parseInt(e.target.value) || 0)}
placeholder="0"
/>
</div>
<div className="space-y-2">
<Label htmlFor="quick-discounted-price">Final Price</Label>
<div className="p-2 bg-gray-50 rounded-md text-sm">
{(quickEditForm.price - (quickEditForm.price * quickEditForm.discount / 100)).toFixed(2)}
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="quick-description">Description</Label>
<Textarea
id="quick-description"
value={quickEditForm.description}
onChange={(e) => handleQuickEditInputChange('description', e.target.value)}
placeholder="Product description"
rows={3}
/>
</div>
{/* Action Buttons */}
<div className="flex items-center justify-end space-x-2 pt-4 border-t">
<Button
variant="outline"
onClick={() => setQuickEditOpen(false)}
disabled={quickEditLoading}
>
Cancel
</Button>
<Button
onClick={handleQuickEditSave}
disabled={quickEditLoading}
>
{quickEditLoading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Saving...
</>
) : (
<>
<Save className="h-4 w-4 mr-2" />
Save Changes
</>
)}
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
)
}

304
app/admin/reviews/page.tsx Normal file
View File

@@ -0,0 +1,304 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Star, Search, Check, X, Eye, Trash2 } from 'lucide-react'
import { toast } from 'sonner'
import { formatDistanceToNow } from 'date-fns'
interface Review {
id: string
rating: number
title?: string
comment?: string
isVerified: boolean
isApproved: boolean
helpfulVotes: number
reportCount: number
createdAt: string
user: {
name: string
email: string
}
product: {
name: string
}
}
export default function AdminReviewsPage() {
const [reviews, setReviews] = useState<Review[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState('')
const [statusFilter, setStatusFilter] = useState<'all' | 'pending' | 'approved'>('all')
const fetchReviews = useCallback(async () => {
try {
setLoading(true)
const params = new URLSearchParams({
page: '1',
limit: '50',
admin: 'true'
})
if (search) params.append('search', search)
if (statusFilter !== 'all') {
params.append('approved', statusFilter === 'approved' ? 'true' : 'false')
}
const response = await fetch(`/api/admin/reviews?${params}`)
const data = await response.json()
if (response.ok) {
setReviews(data.reviews || [])
} else {
toast.error('Failed to load reviews')
}
} catch (error) {
toast.error('Failed to load reviews')
} finally {
setLoading(false)
}
}, [search, statusFilter])
useEffect(() => {
fetchReviews()
}, [fetchReviews])
const handleApprove = async (reviewId: string) => {
try {
const response = await fetch(`/api/admin/reviews/${reviewId}/approve`, {
method: 'POST'
})
if (response.ok) {
toast.success('Review approved')
setReviews(prev =>
prev.map(review =>
review.id === reviewId
? { ...review, isApproved: true }
: review
)
)
} else {
toast.error('Failed to approve review')
}
} catch (error) {
toast.error('Failed to approve review')
}
}
const handleReject = async (reviewId: string) => {
try {
const response = await fetch(`/api/admin/reviews/${reviewId}/reject`, {
method: 'POST'
})
if (response.ok) {
toast.success('Review rejected')
setReviews(prev =>
prev.map(review =>
review.id === reviewId
? { ...review, isApproved: false }
: review
)
)
} else {
toast.error('Failed to reject review')
}
} catch (error) {
toast.error('Failed to reject review')
}
}
const handleDelete = async (reviewId: string) => {
if (!confirm('Are you sure you want to delete this review?')) return
try {
const response = await fetch(`/api/admin/reviews/${reviewId}`, {
method: 'DELETE'
})
if (response.ok) {
toast.success('Review deleted')
setReviews(prev => prev.filter(review => review.id !== reviewId))
} else {
toast.error('Failed to delete review')
}
} catch (error) {
toast.error('Failed to delete review')
}
}
const renderStars = (rating: number) => {
return Array.from({ length: 5 }, (_, index) => (
<Star
key={index}
className={`h-4 w-4 ${
index < rating
? 'text-yellow-400 fill-current'
: 'text-gray-300'
}`}
/>
))
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Reviews Management</h1>
<p className="text-gray-600">Moderate customer reviews and feedback</p>
</div>
</div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>All Reviews</CardTitle>
<div className="flex items-center space-x-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search reviews..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 w-64"
/>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="rounded-md border border-gray-300 px-3 py-2"
>
<option value="all">All Reviews</option>
<option value="pending">Pending Approval</option>
<option value="approved">Approved</option>
</select>
</div>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="text-center py-8">Loading...</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Product</TableHead>
<TableHead>Customer</TableHead>
<TableHead>Rating</TableHead>
<TableHead>Review</TableHead>
<TableHead>Status</TableHead>
<TableHead>Stats</TableHead>
<TableHead>Date</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{reviews.map((review) => (
<TableRow key={review.id}>
<TableCell>
<div>
<p className="font-medium">{review.product.name}</p>
</div>
</TableCell>
<TableCell>
<div>
<p className="font-medium">{review.user.name}</p>
<p className="text-sm text-gray-500">{review.user.email}</p>
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-1">
{renderStars(review.rating)}
<span className="ml-1 text-sm">({review.rating})</span>
</div>
</TableCell>
<TableCell>
<div className="max-w-xs">
{review.title && (
<p className="font-medium text-sm truncate">{review.title}</p>
)}
{review.comment && (
<p className="text-sm text-gray-600 truncate">{review.comment}</p>
)}
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<Badge variant={review.isApproved ? 'default' : 'secondary'}>
{review.isApproved ? 'Approved' : 'Pending'}
</Badge>
{review.isVerified && (
<Badge variant="outline" className="text-green-600 border-green-600">
Verified
</Badge>
)}
</div>
</TableCell>
<TableCell>
<div className="text-sm space-y-1">
<div>👍 {review.helpfulVotes}</div>
{review.reportCount > 0 && (
<div className="text-red-600">🚩 {review.reportCount}</div>
)}
</div>
</TableCell>
<TableCell>
<span className="text-sm text-gray-600">
{formatDistanceToNow(new Date(review.createdAt), { addSuffix: true })}
</span>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
{!review.isApproved && (
<Button
size="sm"
variant="outline"
onClick={() => handleApprove(review.id)}
className="text-green-600 border-green-600"
>
<Check className="h-3 w-3" />
</Button>
)}
{review.isApproved && (
<Button
size="sm"
variant="outline"
onClick={() => handleReject(review.id)}
className="text-orange-600 border-orange-600"
>
<X className="h-3 w-3" />
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={() => handleDelete(review.id)}
className="text-red-600 border-red-600"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
)
}

732
app/admin/settings/page.tsx Normal file
View File

@@ -0,0 +1,732 @@
'use client'
import { useState, useEffect } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import { Switch } from '@/components/ui/switch'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import {
Settings,
DollarSign,
Users,
Package,
Mail,
Shield,
Database,
Plus,
Edit,
Trash2,
Save,
RefreshCw
} from 'lucide-react'
import { motion } from 'framer-motion'
import { toast } from 'sonner'
interface CommissionSetting {
id: string
level: number
percentage: number
isActive: boolean
}
interface Category {
id: string
name: string
description: string | null
image: string | null
isActive: boolean
productCount: number
}
interface SystemSettings {
siteName: string
siteDescription: string
supportEmail: string
minimumPayout: number
enableReferrals: boolean
enableCommissions: boolean
maintenanceMode: boolean
allowRegistration: boolean
}
export default function AdminSettingsPage() {
const [commissionSettings, setCommissionSettings] = useState<CommissionSetting[]>([])
const [categories, setCategories] = useState<Category[]>([])
const [systemSettings, setSystemSettings] = useState<SystemSettings>({
siteName: 'Padmaaja Rasooi',
siteDescription: 'Premium quality rice products and grains. Experience the finest rice sourced directly from local farmers.',
supportEmail: 'info@padmajarice.com',
minimumPayout: 1000,
enableReferrals: true,
enableCommissions: true,
maintenanceMode: false,
allowRegistration: true
})
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [editingCommission, setEditingCommission] = useState<CommissionSetting | null>(null)
const [editingCategory, setEditingCategory] = useState<Category | null>(null)
const [newCommission, setNewCommission] = useState({ level: 1, percentage: 10, isActive: true })
const [newCategory, setNewCategory] = useState({ name: '', description: '', image: '' })
useEffect(() => {
fetchSettings()
}, [])
const fetchSettings = async () => {
try {
setLoading(true)
const [commissionsRes, categoriesRes, systemRes] = await Promise.all([
fetch('/api/admin/settings/commissions'),
fetch('/api/admin/settings/categories'),
fetch('/api/admin/settings/system')
])
const [commissionsData, categoriesData, systemData] = await Promise.all([
commissionsRes.json(),
categoriesRes.json(),
systemRes.json()
])
setCommissionSettings(commissionsData.settings || [])
setCategories(categoriesData.categories || [])
setSystemSettings(prev => ({ ...prev, ...systemData }))
} catch (error) {
console.error('Error fetching settings:', error)
toast.error('Failed to load settings')
} finally {
setLoading(false)
}
}
const saveSystemSettings = async () => {
try {
setSaving(true)
const response = await fetch('/api/admin/settings/system', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(systemSettings)
})
if (!response.ok) throw new Error('Failed to save settings')
toast.success('System settings saved successfully')
// Show warning if maintenance mode was enabled
if (systemSettings.maintenanceMode) {
toast.warning('⚠️ Maintenance mode enabled! Public site is now offline.')
} else {
toast.success('✅ Maintenance mode disabled. Public site is now online.')
}
} catch (error) {
console.error('Error saving system settings:', error)
toast.error('Failed to save system settings')
} finally {
setSaving(false)
}
}
const saveCommissionSetting = async (setting: CommissionSetting) => {
try {
const response = await fetch(`/api/admin/settings/commissions/${setting.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(setting)
})
if (!response.ok) throw new Error('Failed to save commission setting')
toast.success('Commission setting saved successfully')
fetchSettings()
setEditingCommission(null)
} catch (error) {
console.error('Error saving commission setting:', error)
toast.error('Failed to save commission setting')
}
}
const addCommissionSetting = async () => {
try {
const response = await fetch('/api/admin/settings/commissions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newCommission)
})
if (!response.ok) throw new Error('Failed to add commission setting')
toast.success('Commission setting added successfully')
setNewCommission({ level: 1, percentage: 10, isActive: true })
fetchSettings()
} catch (error) {
console.error('Error adding commission setting:', error)
toast.error('Failed to add commission setting')
}
}
const deleteCommissionSetting = async (id: string) => {
try {
const response = await fetch(`/api/admin/settings/commissions/${id}`, {
method: 'DELETE'
})
if (!response.ok) throw new Error('Failed to delete commission setting')
toast.success('Commission setting deleted successfully')
fetchSettings()
} catch (error) {
console.error('Error deleting commission setting:', error)
toast.error('Failed to delete commission setting')
}
}
const saveCategory = async (category: Category) => {
try {
const url = category.id.startsWith('new')
? '/api/admin/settings/categories'
: `/api/admin/settings/categories/${category.id}`
const response = await fetch(url, {
method: category.id.startsWith('new') ? 'POST' : 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: category.name,
description: category.description,
image: category.image,
isActive: category.isActive
})
})
if (!response.ok) throw new Error('Failed to save category')
toast.success('Category saved successfully')
fetchSettings()
setEditingCategory(null)
setNewCategory({ name: '', description: '', image: '' })
} catch (error) {
console.error('Error saving category:', error)
toast.error('Failed to save category')
}
}
const deleteCategory = async (id: string) => {
try {
const response = await fetch(`/api/admin/settings/categories/${id}`, {
method: 'DELETE'
})
if (!response.ok) throw new Error('Failed to delete category')
toast.success('Category deleted successfully')
fetchSettings()
} catch (error) {
console.error('Error deleting category:', error)
toast.error('Failed to delete category')
}
}
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
<div className="w-16 h-16 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100 p-6">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header with maintenance warning */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-4"
>
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-bold bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text text-transparent">
System Settings
</h1>
<p className="text-gray-600 mt-2">Manage your platform configuration and settings</p>
</div>
<Button onClick={() => fetchSettings()} variant="outline">
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
</div>
{systemSettings.maintenanceMode && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center space-x-2">
<Settings className="h-5 w-5 text-red-600" />
<h3 className="font-medium text-red-900">Maintenance Mode Active</h3>
</div>
<div className="mt-2 space-y-1">
<p className="text-sm text-red-700">
The public website is currently offline for regular users.
</p>
<p className="text-sm text-red-600 font-medium">
Admin users (like you) can still access all pages normally.
</p>
</div>
</div>
)}
</motion.div>
<Tabs defaultValue="system" className="space-y-6">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="system" className="flex items-center space-x-2">
<Settings className="h-4 w-4" />
<span>System</span>
</TabsTrigger>
<TabsTrigger value="commissions" className="flex items-center space-x-2">
<DollarSign className="h-4 w-4" />
<span>Commissions</span>
</TabsTrigger>
<TabsTrigger value="categories" className="flex items-center space-x-2">
<Package className="h-4 w-4" />
<span>Categories</span>
</TabsTrigger>
<TabsTrigger value="security" className="flex items-center space-x-2">
<Shield className="h-4 w-4" />
<span>Security</span>
</TabsTrigger>
</TabsList>
{/* System Settings */}
<TabsContent value="system">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-6"
>
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Settings className="h-5 w-5" />
<span>General Settings</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label htmlFor="siteName">Site Name</Label>
<Input
id="siteName"
value={systemSettings.siteName}
onChange={(e) => setSystemSettings(prev => ({ ...prev, siteName: e.target.value }))}
/>
</div>
<div>
<Label htmlFor="supportEmail">Support Email</Label>
<Input
id="supportEmail"
type="email"
value={systemSettings.supportEmail}
onChange={(e) => setSystemSettings(prev => ({ ...prev, supportEmail: e.target.value }))}
/>
</div>
</div>
<div>
<Label htmlFor="siteDescription">Site Description</Label>
<Textarea
id="siteDescription"
value={systemSettings.siteDescription}
onChange={(e) => setSystemSettings(prev => ({ ...prev, siteDescription: e.target.value }))}
rows={3}
/>
</div>
<div>
<Label htmlFor="minimumPayout">Minimum Payout Amount ()</Label>
<Input
id="minimumPayout"
type="number"
value={systemSettings.minimumPayout}
onChange={(e) => setSystemSettings(prev => ({ ...prev, minimumPayout: parseFloat(e.target.value) }))}
/>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>Enable Referral System</Label>
<p className="text-sm text-gray-500">Allow users to refer others</p>
</div>
<Switch
checked={systemSettings.enableReferrals}
onCheckedChange={(checked) => setSystemSettings(prev => ({ ...prev, enableReferrals: checked }))}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label>Enable Commissions</Label>
<p className="text-sm text-gray-500">Enable commission calculations</p>
</div>
<Switch
checked={systemSettings.enableCommissions}
onCheckedChange={(checked) => setSystemSettings(prev => ({ ...prev, enableCommissions: checked }))}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label>Allow Registration</Label>
<p className="text-sm text-gray-500">Allow new user registrations</p>
</div>
<Switch
checked={systemSettings.allowRegistration}
onCheckedChange={(checked) => setSystemSettings(prev => ({ ...prev, allowRegistration: checked }))}
/>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg bg-red-50 border-red-200">
<div>
<Label className="text-red-900 font-medium">Maintenance Mode</Label>
<p className="text-sm text-red-700 mt-1">Put site in maintenance mode</p>
<p className="text-xs text-red-600 font-medium">
This will take the public site offline for regular users. Admin users will still have full access.
</p>
</div>
<Switch
checked={systemSettings.maintenanceMode}
onCheckedChange={(checked) => setSystemSettings(prev => ({ ...prev, maintenanceMode: checked }))}
/>
</div>
</div>
<Button onClick={saveSystemSettings} disabled={saving}>
<Save className="h-4 w-4 mr-2" />
{saving ? 'Saving...' : 'Save Settings'}
</Button>
</CardContent>
</Card>
</motion.div>
</TabsContent>
{/* Commission Settings */}
<TabsContent value="commissions">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-6"
>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center space-x-2">
<DollarSign className="h-5 w-5" />
<span>Commission Structure</span>
</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
Add Level
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Commission Level</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Level</Label>
<Input
type="number"
value={newCommission.level}
onChange={(e) => setNewCommission(prev => ({ ...prev, level: parseInt(e.target.value) }))}
/>
</div>
<div>
<Label>Percentage (%)</Label>
<Input
type="number"
step="0.1"
value={newCommission.percentage}
onChange={(e) => setNewCommission(prev => ({ ...prev, percentage: parseFloat(e.target.value) }))}
/>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={newCommission.isActive}
onCheckedChange={(checked) => setNewCommission(prev => ({ ...prev, isActive: checked }))}
/>
<Label>Active</Label>
</div>
<Button onClick={addCommissionSetting} className="w-full">
Add Commission Level
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
{commissionSettings.map((setting) => (
<div key={setting.id} className="flex items-center justify-between p-4 border rounded-lg">
<div>
<p className="font-medium">Level {setting.level}</p>
<p className="text-sm text-gray-500">{setting.percentage}% commission</p>
</div>
<div className="flex items-center space-x-2">
<Badge variant={setting.isActive ? 'default' : 'secondary'}>
{setting.isActive ? 'Active' : 'Inactive'}
</Badge>
<Button
variant="outline"
size="sm"
onClick={() => setEditingCommission(setting)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => deleteCommissionSetting(setting.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</motion.div>
</TabsContent>
{/* Categories */}
<TabsContent value="categories">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-6"
>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center space-x-2">
<Package className="h-5 w-5" />
<span>Product Categories</span>
</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
Add Category
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Category</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Name</Label>
<Input
value={newCategory.name}
onChange={(e) => setNewCategory(prev => ({ ...prev, name: e.target.value }))}
/>
</div>
<div>
<Label>Description</Label>
<Textarea
value={newCategory.description}
onChange={(e) => setNewCategory(prev => ({ ...prev, description: e.target.value }))}
rows={3}
/>
</div>
<div>
<Label>Image URL</Label>
<Input
value={newCategory.image}
onChange={(e) => setNewCategory(prev => ({ ...prev, image: e.target.value }))}
/>
</div>
<Button
onClick={() => saveCategory({
id: 'new-category',
name: newCategory.name,
description: newCategory.description,
image: newCategory.image,
isActive: true,
productCount: 0
})}
className="w-full"
>
Add Category
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{categories.map((category) => (
<div key={category.id} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h3 className="font-medium">{category.name}</h3>
<Badge variant={category.isActive ? 'default' : 'secondary'}>
{category.isActive ? 'Active' : 'Inactive'}
</Badge>
</div>
<p className="text-sm text-gray-500 mb-2">{category.description}</p>
<p className="text-xs text-gray-400 mb-3">{category.productCount} products</p>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setEditingCategory(category)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => deleteCategory(category.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</motion.div>
</TabsContent>
{/* Security */}
<TabsContent value="security">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-6"
>
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Shield className="h-5 w-5" />
<span>Security Settings</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h3 className="font-medium text-blue-900 mb-2">Database Status</h3>
<p className="text-sm text-blue-700">Database connection is healthy</p>
</div>
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<h3 className="font-medium text-yellow-900 mb-2">Environment Variables</h3>
<p className="text-sm text-yellow-700">All required environment variables are configured</p>
</div>
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<h3 className="font-medium text-green-900 mb-2">SSL Certificate</h3>
<p className="text-sm text-green-700">SSL certificate is valid and up to date</p>
</div>
</div>
</CardContent>
</Card>
</motion.div>
</TabsContent>
</Tabs>
{/* Edit Dialogs */}
{editingCommission && (
<Dialog open={!!editingCommission} onOpenChange={() => setEditingCommission(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Commission Setting</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Level</Label>
<Input
type="number"
value={editingCommission.level}
onChange={(e) => setEditingCommission(prev => prev ? { ...prev, level: parseInt(e.target.value) } : null)}
/>
</div>
<div>
<Label>Percentage (%)</Label>
<Input
type="number"
step="0.1"
value={editingCommission.percentage}
onChange={(e) => setEditingCommission(prev => prev ? { ...prev, percentage: parseFloat(e.target.value) } : null)}
/>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={editingCommission.isActive}
onCheckedChange={(checked) => setEditingCommission(prev => prev ? { ...prev, isActive: checked } : null)}
/>
<Label>Active</Label>
</div>
<Button onClick={() => saveCommissionSetting(editingCommission)} className="w-full">
Save Changes
</Button>
</div>
</DialogContent>
</Dialog>
)}
{editingCategory && (
<Dialog open={!!editingCategory} onOpenChange={() => setEditingCategory(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Category</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Name</Label>
<Input
value={editingCategory.name}
onChange={(e) => setEditingCategory(prev => prev ? { ...prev, name: e.target.value } : null)}
/>
</div>
<div>
<Label>Description</Label>
<Textarea
value={editingCategory.description || ''}
onChange={(e) => setEditingCategory(prev => prev ? { ...prev, description: e.target.value } : null)}
rows={3}
/>
</div>
<div>
<Label>Image URL</Label>
<Input
value={editingCategory.image || ''}
onChange={(e) => setEditingCategory(prev => prev ? { ...prev, image: e.target.value } : null)}
/>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={editingCategory.isActive}
onCheckedChange={(checked) => setEditingCategory(prev => prev ? { ...prev, isActive: checked } : null)}
/>
<Label>Active</Label>
</div>
<Button onClick={() => saveCategory(editingCategory)} className="w-full">
Save Changes
</Button>
</div>
</DialogContent>
</Dialog>
)}
</div>
</div>
)
}

190
app/admin/users/page.tsx Normal file
View File

@@ -0,0 +1,190 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Search, Filter, Download, UserPlus } from 'lucide-react'
interface User {
id: string
name: string
email: string
role: 'ADMIN' | 'MEMBER' | 'CUSTOMER' | 'WHOLESALER' | 'PART_TIME'
isActive: boolean
joinedAt: string
referralCode: string
_count: {
referrals: number
orders: number
}
referrer?: {
name: string
email: string
}
}
export default function AdminUsersPage() {
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState('')
const [roleFilter, setRoleFilter] = useState('all')
const [page, setPage] = useState(1)
const fetchUsers = useCallback(async () => {
try {
setLoading(true)
const params = new URLSearchParams({
page: page.toString(),
limit: '10'
})
if (search) params.append('search', search)
if (roleFilter && roleFilter !== 'all') params.append('role', roleFilter)
const response = await fetch(`/api/admin/users?${params}`)
const data = await response.json()
setUsers(data.users || [])
} catch (error) {
console.error('Error fetching users:', error)
} finally {
setLoading(false)
}
}, [search, roleFilter, page])
useEffect(() => {
fetchUsers()
}, [fetchUsers])
const getRoleBadgeColor = (role: string) => {
switch (role) {
case 'ADMIN': return 'bg-red-500'
case 'MEMBER': return 'bg-blue-500'
case 'CUSTOMER': return 'bg-gray-500'
default: return 'bg-gray-500'
}
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Users</h1>
<p className="text-gray-600">Manage your platform users</p>
</div>
<Button>
<UserPlus className="h-4 w-4 mr-2" />
Add User
</Button>
</div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>All Users</CardTitle>
<div className="flex items-center space-x-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search users..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 w-64"
/>
</div>
<Select value={roleFilter} onValueChange={setRoleFilter}>
<SelectTrigger className="w-32">
<SelectValue placeholder="Role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Roles</SelectItem>
<SelectItem value="ADMIN">Admin</SelectItem>
<SelectItem value="MEMBER">Member</SelectItem>
<SelectItem value="CUSTOMER">Customer</SelectItem>
<SelectItem value="WHOLESALER">Wholesaler</SelectItem>
<SelectItem value="PART_TIME">Part Time</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
Export
</Button>
</div>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="text-center py-8">Loading...</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>User</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
<TableHead>Referrals</TableHead>
<TableHead>Orders</TableHead>
<TableHead>Joined</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>
<div>
<p className="font-medium">{user.name}</p>
<p className="text-sm text-gray-500">{user.email}</p>
{user.referrer && (
<p className="text-xs text-gray-400">
Referred by: {user.referrer.name}
</p>
)}
</div>
</TableCell>
<TableCell>
<Badge className={`${getRoleBadgeColor(user.role)} text-white`}>
{user.role}
</Badge>
</TableCell>
<TableCell>
<Badge variant={user.isActive ? 'default' : 'secondary'}>
{user.isActive ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell>{user._count.referrals}</TableCell>
<TableCell>{user._count.orders}</TableCell>
<TableCell>
{new Date(user.joinedAt).toLocaleDateString()}
</TableCell>
<TableCell>
<Button variant="ghost" size="sm">
Edit
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
)
}