Add admin dashboard pages and restructure admin route
Introduces new admin dashboard pages for alerts, bans, invites, orders, settings, status, and vendors under app/dashboard/admin/. Moves the main admin page to the new dashboard structure and adds a shared admin layout. Updates sidebar configuration and adds supporting components and hooks for admin features.
This commit is contained in:
313
app/dashboard/admin/alerts/page.tsx
Normal file
313
app/dashboard/admin/alerts/page.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
import React from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { AlertTriangle, CheckCircle, XCircle, Clock, Bell, Shield } from "lucide-react";
|
||||
|
||||
export default function AdminAlertsPage() {
|
||||
// Mock data for system alerts
|
||||
const alerts = [
|
||||
{
|
||||
id: "1",
|
||||
type: "security",
|
||||
severity: "high",
|
||||
title: "Suspicious Login Attempts",
|
||||
description: "Multiple failed login attempts detected from IP 192.168.1.100",
|
||||
timestamp: "2024-01-20 14:30:00",
|
||||
status: "active",
|
||||
affectedUsers: 3
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "system",
|
||||
severity: "medium",
|
||||
title: "High Memory Usage",
|
||||
description: "Server memory usage has exceeded 85% for the past hour",
|
||||
timestamp: "2024-01-20 13:45:00",
|
||||
status: "resolved",
|
||||
affectedUsers: 0
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "payment",
|
||||
severity: "high",
|
||||
title: "Payment Processing Error",
|
||||
description: "Bitcoin payment gateway experiencing delays",
|
||||
timestamp: "2024-01-20 12:15:00",
|
||||
status: "active",
|
||||
affectedUsers: 12
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
type: "user",
|
||||
severity: "low",
|
||||
title: "New Vendor Registration",
|
||||
description: "New vendor 'tech_supplies' has registered and requires approval",
|
||||
timestamp: "2024-01-20 11:20:00",
|
||||
status: "pending",
|
||||
affectedUsers: 1
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
type: "security",
|
||||
severity: "critical",
|
||||
title: "Potential Security Breach",
|
||||
description: "Unusual API access patterns detected from multiple IPs",
|
||||
timestamp: "2024-01-20 10:30:00",
|
||||
status: "investigating",
|
||||
affectedUsers: 0
|
||||
}
|
||||
];
|
||||
|
||||
const getSeverityColor = (severity: string) => {
|
||||
switch (severity) {
|
||||
case "critical": return "destructive";
|
||||
case "high": return "destructive";
|
||||
case "medium": return "secondary";
|
||||
case "low": return "outline";
|
||||
default: return "outline";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "active": return "destructive";
|
||||
case "resolved": return "default";
|
||||
case "pending": return "secondary";
|
||||
case "investigating": return "outline";
|
||||
default: return "outline";
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "security": return <Shield className="h-4 w-4" />;
|
||||
case "system": return <AlertTriangle className="h-4 w-4" />;
|
||||
case "payment": return <Bell className="h-4 w-4" />;
|
||||
case "user": return <CheckCircle className="h-4 w-4" />;
|
||||
default: return <AlertTriangle className="h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">System Alerts</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">Monitor system alerts and security notifications</p>
|
||||
</div>
|
||||
|
||||
{/* Alert Summary */}
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Active Alerts</CardTitle>
|
||||
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">8</div>
|
||||
<p className="text-xs text-muted-foreground">Require attention</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Critical</CardTitle>
|
||||
<XCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">2</div>
|
||||
<p className="text-xs text-muted-foreground">Immediate action needed</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Resolved Today</CardTitle>
|
||||
<CheckCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">15</div>
|
||||
<p className="text-xs text-muted-foreground">Successfully resolved</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Avg Response</CardTitle>
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">12m</div>
|
||||
<p className="text-xs text-muted-foreground">Average resolution time</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Critical Alerts */}
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<strong>Critical Alert:</strong> Potential security breach detected. Multiple unusual API access patterns from different IP addresses. Immediate investigation required.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* Alerts Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>System Alerts</CardTitle>
|
||||
<CardDescription>Recent system alerts and notifications</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="outline" size="sm">
|
||||
Mark All Read
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Clear Resolved
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Alert</TableHead>
|
||||
<TableHead>Severity</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Affected Users</TableHead>
|
||||
<TableHead>Timestamp</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{alerts.map((alert) => (
|
||||
<TableRow key={alert.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-2">
|
||||
{getTypeIcon(alert.type)}
|
||||
<span className="capitalize">{alert.type}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className="font-medium">{alert.title}</div>
|
||||
<div className="text-sm text-muted-foreground max-w-[300px] truncate">
|
||||
{alert.description}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={getSeverityColor(alert.severity)}>
|
||||
{alert.severity}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={getStatusColor(alert.status)}>
|
||||
{alert.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{alert.affectedUsers}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{alert.timestamp}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<Button variant="outline" size="sm">
|
||||
View Details
|
||||
</Button>
|
||||
{alert.status === "active" && (
|
||||
<Button variant="outline" size="sm">
|
||||
Resolve
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Alert Categories */}
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Shield className="h-5 w-5 mr-2" />
|
||||
Security Alerts
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">Failed Logins</span>
|
||||
<Badge variant="destructive">3</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">Suspicious Activity</span>
|
||||
<Badge variant="destructive">1</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">API Abuse</span>
|
||||
<Badge variant="secondary">0</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<AlertTriangle className="h-5 w-5 mr-2" />
|
||||
System Alerts
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">High CPU Usage</span>
|
||||
<Badge variant="secondary">1</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">Memory Issues</span>
|
||||
<Badge variant="default">0</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">Database Slow</span>
|
||||
<Badge variant="secondary">0</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Bell className="h-5 w-5 mr-2" />
|
||||
Payment Alerts
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">Gateway Issues</span>
|
||||
<Badge variant="destructive">1</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">Failed Transactions</span>
|
||||
<Badge variant="secondary">2</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">High Volume</span>
|
||||
<Badge variant="outline">0</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
280
app/dashboard/admin/ban/page.tsx
Normal file
280
app/dashboard/admin/ban/page.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
import React from "react";
|
||||
import { Card, CardContent, CardDescription, 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
||||
import { UserX, Shield, Search, Ban, Unlock } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function AdminBanPage() {
|
||||
const [banData, setBanData] = useState({
|
||||
username: "",
|
||||
reason: "",
|
||||
duration: "",
|
||||
description: ""
|
||||
});
|
||||
|
||||
// Mock data for banned users
|
||||
const bannedUsers = [
|
||||
{
|
||||
id: "1",
|
||||
username: "spam_user",
|
||||
email: "spam@example.com",
|
||||
reason: "Spam",
|
||||
bannedBy: "admin1",
|
||||
banDate: "2024-01-15",
|
||||
duration: "Permanent",
|
||||
status: "active"
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
username: "fraud_vendor",
|
||||
email: "fraud@example.com",
|
||||
reason: "Fraud",
|
||||
bannedBy: "admin1",
|
||||
banDate: "2024-01-20",
|
||||
duration: "30 days",
|
||||
status: "active"
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
username: "policy_violator",
|
||||
email: "violator@example.com",
|
||||
reason: "Policy Violation",
|
||||
bannedBy: "admin1",
|
||||
banDate: "2024-01-25",
|
||||
duration: "7 days",
|
||||
status: "expired"
|
||||
}
|
||||
];
|
||||
|
||||
const handleBanUser = () => {
|
||||
// Handle ban user logic
|
||||
console.log("Banning user:", banData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Ban Users</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">Manage user bans and suspensions</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Active Bans</CardTitle>
|
||||
<Shield className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">12</div>
|
||||
<p className="text-xs text-muted-foreground">Currently banned</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Permanent Bans</CardTitle>
|
||||
<UserX className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">3</div>
|
||||
<p className="text-xs text-muted-foreground">Permanent suspensions</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Temporary Bans</CardTitle>
|
||||
<Ban className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">9</div>
|
||||
<p className="text-xs text-muted-foreground">Time-limited bans</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Appeals Pending</CardTitle>
|
||||
<Unlock className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">2</div>
|
||||
<p className="text-xs text-muted-foreground">Awaiting review</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{/* Ban User Form */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<UserX className="h-5 w-5 mr-2" />
|
||||
Ban User
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Enter user details and reason for banning
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input
|
||||
id="username"
|
||||
placeholder="Enter username to ban"
|
||||
value={banData.username}
|
||||
onChange={(e) => setBanData({...banData, username: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reason">Reason</Label>
|
||||
<Select value={banData.reason} onValueChange={(value) => setBanData({...banData, reason: value})}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select ban reason" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="spam">Spam</SelectItem>
|
||||
<SelectItem value="fraud">Fraud</SelectItem>
|
||||
<SelectItem value="harassment">Harassment</SelectItem>
|
||||
<SelectItem value="policy_violation">Policy Violation</SelectItem>
|
||||
<SelectItem value="suspicious_activity">Suspicious Activity</SelectItem>
|
||||
<SelectItem value="other">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="duration">Duration</Label>
|
||||
<Select value={banData.duration} onValueChange={(value) => setBanData({...banData, duration: value})}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select ban duration" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1_day">1 Day</SelectItem>
|
||||
<SelectItem value="7_days">7 Days</SelectItem>
|
||||
<SelectItem value="30_days">30 Days</SelectItem>
|
||||
<SelectItem value="90_days">90 Days</SelectItem>
|
||||
<SelectItem value="permanent">Permanent</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Additional Details</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Provide additional context for the ban..."
|
||||
value={banData.description}
|
||||
onChange={(e) => setBanData({...banData, description: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" className="w-full">
|
||||
<Ban className="h-4 w-4 mr-2" />
|
||||
Ban User
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirm Ban</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to ban user "{banData.username}"? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleBanUser}>
|
||||
Confirm Ban
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Search Banned Users */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Search Banned Users</CardTitle>
|
||||
<CardDescription>Look up existing bans and their status</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input placeholder="Search by username or email..." className="pl-8" />
|
||||
</div>
|
||||
<Button variant="outline" className="w-full">
|
||||
Search Bans
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Banned Users Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Bans</CardTitle>
|
||||
<CardDescription>View and manage current and past bans</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Reason</TableHead>
|
||||
<TableHead>Duration</TableHead>
|
||||
<TableHead>Banned By</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{bannedUsers.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className="font-medium">{user.username}</div>
|
||||
<div className="text-sm text-muted-foreground">{user.email}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{user.reason}</TableCell>
|
||||
<TableCell>{user.duration}</TableCell>
|
||||
<TableCell>{user.bannedBy}</TableCell>
|
||||
<TableCell>{user.banDate}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={user.status === "active" ? "destructive" : "secondary"}
|
||||
>
|
||||
{user.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
{user.status === "active" && (
|
||||
<Button variant="outline" size="sm">
|
||||
<Unlock className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" size="sm">
|
||||
View Details
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
202
app/dashboard/admin/invite/page.tsx
Normal file
202
app/dashboard/admin/invite/page.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Card, CardContent, CardDescription, 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { UserPlus, Mail, Copy, Check } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function AdminInvitePage() {
|
||||
const [inviteData, setInviteData] = useState({
|
||||
email: "",
|
||||
username: "",
|
||||
role: "",
|
||||
message: ""
|
||||
});
|
||||
const [inviteLink, setInviteLink] = useState("");
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleInvite = () => {
|
||||
// Generate invite link (mock implementation)
|
||||
const link = `https://ember-market.com/invite/${Math.random().toString(36).substr(2, 9)}`;
|
||||
setInviteLink(link);
|
||||
};
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(inviteLink);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Invite Vendor</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">Send invitations to new vendors to join the platform</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{/* Invite Form */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<UserPlus className="h-5 w-5 mr-2" />
|
||||
Send Invitation
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Fill out the details to send an invitation to a new vendor
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email Address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="vendor@example.com"
|
||||
value={inviteData.email}
|
||||
onChange={(e) => setInviteData({...inviteData, email: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input
|
||||
id="username"
|
||||
placeholder="vendor_username"
|
||||
value={inviteData.username}
|
||||
onChange={(e) => setInviteData({...inviteData, username: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="role">Role</Label>
|
||||
<Select value={inviteData.role} onValueChange={(value) => setInviteData({...inviteData, role: value})}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select vendor role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="standard">Standard Vendor</SelectItem>
|
||||
<SelectItem value="premium">Premium Vendor</SelectItem>
|
||||
<SelectItem value="enterprise">Enterprise Vendor</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="message">Personal Message (Optional)</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
placeholder="Add a personal message to the invitation..."
|
||||
value={inviteData.message}
|
||||
onChange={(e) => setInviteData({...inviteData, message: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleInvite} className="w-full">
|
||||
<Mail className="h-4 w-4 mr-2" />
|
||||
Send Invitation
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Invite Link */}
|
||||
{inviteLink && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Invitation Link</CardTitle>
|
||||
<CardDescription>
|
||||
Share this link with the vendor to complete their registration
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="p-3 bg-muted rounded-md">
|
||||
<code className="text-sm break-all">{inviteLink}</code>
|
||||
</div>
|
||||
<Button
|
||||
onClick={copyToClipboard}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
Copy Link
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Recent Invitations */}
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Invitations</CardTitle>
|
||||
<CardDescription>Track the status of sent invitations</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<Mail className="h-5 w-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">john.doe@example.com</p>
|
||||
<p className="text-sm text-muted-foreground">Sent 2 hours ago</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="outline">Pending</Badge>
|
||||
<Button variant="outline" size="sm">Resend</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<UserPlus className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">jane.smith@example.com</p>
|
||||
<p className="text-sm text-muted-foreground">Accepted 1 day ago</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="default" className="bg-green-500">Accepted</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center">
|
||||
<Mail className="h-5 w-5 text-red-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">bob.wilson@example.com</p>
|
||||
<p className="text-sm text-muted-foreground">Expired 3 days ago</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="destructive">Expired</Badge>
|
||||
<Button variant="outline" size="sm">Resend</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
6
app/dashboard/admin/layout.tsx
Normal file
6
app/dashboard/admin/layout.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import type React from "react"
|
||||
import Layout from "@/components/layout/layout"
|
||||
|
||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
return <Layout>{children}</Layout>
|
||||
}
|
||||
205
app/dashboard/admin/orders/page.tsx
Normal file
205
app/dashboard/admin/orders/page.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import React from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Package, AlertTriangle } from "lucide-react";
|
||||
import { fetchServer } from "@/lib/api";
|
||||
import OrdersTable from "@/components/admin/OrdersTable";
|
||||
|
||||
interface Order {
|
||||
orderId: string | number;
|
||||
userId: string;
|
||||
total: number;
|
||||
createdAt: string;
|
||||
status: string;
|
||||
items: Array<{
|
||||
name: string;
|
||||
quantity: number;
|
||||
}>;
|
||||
vendorUsername?: string;
|
||||
}
|
||||
|
||||
interface SystemStats {
|
||||
vendors: number;
|
||||
orders: number;
|
||||
products: number;
|
||||
chats: number;
|
||||
}
|
||||
|
||||
|
||||
export default async function AdminOrdersPage() {
|
||||
let orders: Order[] = [];
|
||||
let systemStats: SystemStats | null = null;
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
const [ordersData, statsData] = await Promise.all([
|
||||
fetchServer<Order[]>("/admin/recent-orders"),
|
||||
fetchServer<SystemStats>("/admin/stats")
|
||||
]);
|
||||
orders = ordersData;
|
||||
systemStats = statsData;
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch data:", err);
|
||||
error = "Failed to load data";
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Recent Orders</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">Monitor and manage platform orders</p>
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center text-red-500">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const acknowledgedOrders = orders.filter(o => o.status === 'acknowledged');
|
||||
const paidOrders = orders.filter(o => o.status === 'paid');
|
||||
const completedOrders = orders.filter(o => o.status === 'completed');
|
||||
const cancelledOrders = orders.filter(o => o.status === 'cancelled');
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Recent Orders</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">Monitor and manage platform orders</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Orders</CardTitle>
|
||||
<Package className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{systemStats?.orders || 0}</div>
|
||||
<p className="text-xs text-muted-foreground">All platform orders</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Acknowledged</CardTitle>
|
||||
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{acknowledgedOrders.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Vendor accepted</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Paid</CardTitle>
|
||||
<Package className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{paidOrders.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Payment received</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Completed</CardTitle>
|
||||
<Package className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{completedOrders.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Successfully delivered</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Orders Table with Pagination */}
|
||||
<OrdersTable orders={orders} />
|
||||
|
||||
{/* Order Analytics */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Order Status Distribution</CardTitle>
|
||||
<CardDescription>Breakdown of recent orders by status</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-3 h-3 bg-purple-500 rounded-full"></div>
|
||||
<span className="text-sm">Acknowledged</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
{orders.length > 0 ? Math.round((acknowledgedOrders.length / orders.length) * 100) : 0}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-3 h-3 bg-emerald-500 rounded-full"></div>
|
||||
<span className="text-sm">Paid</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
{orders.length > 0 ? Math.round((paidOrders.length / orders.length) * 100) : 0}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||
<span className="text-sm">Completed</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
{orders.length > 0 ? Math.round((completedOrders.length / orders.length) * 100) : 0}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
|
||||
<span className="text-sm">Cancelled</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
{orders.length > 0 ? Math.round((cancelledOrders.length / orders.length) * 100) : 0}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Order Summary</CardTitle>
|
||||
<CardDescription>Recent order activity breakdown</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Total Recent Orders</span>
|
||||
<span className="text-sm font-medium">{orders.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Acknowledged</span>
|
||||
<span className="text-sm font-medium">{acknowledgedOrders.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Paid</span>
|
||||
<span className="text-sm font-medium">{paidOrders.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Completed</span>
|
||||
<span className="text-sm font-medium">{completedOrders.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Cancelled</span>
|
||||
<span className="text-sm font-medium">{cancelledOrders.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import React from "react";
|
||||
import InviteVendorCard from "@/components/admin/InviteVendorCard";
|
||||
import BanUserCard from "@/components/admin/BanUserCard";
|
||||
import RecentOrdersCard from "@/components/admin/RecentOrdersCard";
|
||||
@@ -9,7 +10,7 @@ import VendorsCard from "@/components/admin/VendorsCard";
|
||||
|
||||
export default function AdminPage() {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Admin</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">Restricted area. Only admin1 can access.</p>
|
||||
333
app/dashboard/admin/settings/page.tsx
Normal file
333
app/dashboard/admin/settings/page.tsx
Normal file
@@ -0,0 +1,333 @@
|
||||
import React from "react";
|
||||
import { Card, CardContent, CardDescription, 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 { Switch } from "@/components/ui/switch";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Settings, Shield, Bell, Database, Globe, Key, Save } from "lucide-react";
|
||||
|
||||
export default function AdminSettingsPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Admin Settings</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">Configure system settings and preferences</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{/* General Settings */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Settings className="h-5 w-5 mr-2" />
|
||||
General Settings
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Basic platform configuration and preferences
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="siteName">Site Name</Label>
|
||||
<Input id="siteName" defaultValue="Ember Market" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="siteDescription">Site Description</Label>
|
||||
<Textarea
|
||||
id="siteDescription"
|
||||
defaultValue="A secure cryptocurrency marketplace for vendors and customers"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="defaultCurrency">Default Currency</Label>
|
||||
<Select defaultValue="btc">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="btc">Bitcoin (BTC)</SelectItem>
|
||||
<SelectItem value="eth">Ethereum (ETH)</SelectItem>
|
||||
<SelectItem value="ltc">Litecoin (LTC)</SelectItem>
|
||||
<SelectItem value="usdt">Tether (USDT)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Maintenance Mode</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Temporarily disable public access
|
||||
</p>
|
||||
</div>
|
||||
<Switch />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Security Settings */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Shield className="h-5 w-5 mr-2" />
|
||||
Security Settings
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configure security policies and authentication
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Two-Factor Authentication</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Require 2FA for all admin accounts
|
||||
</p>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Session Timeout</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Auto-logout after inactivity
|
||||
</p>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sessionDuration">Session Duration (minutes)</Label>
|
||||
<Input id="sessionDuration" type="number" defaultValue="60" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>IP Whitelist</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Restrict admin access to specific IPs
|
||||
</p>
|
||||
</div>
|
||||
<Switch />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="allowedIPs">Allowed IP Addresses</Label>
|
||||
<Textarea
|
||||
id="allowedIPs"
|
||||
placeholder="192.168.1.100 10.0.0.50"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Notification Settings */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Bell className="h-5 w-5 mr-2" />
|
||||
Notification Settings
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configure alert and notification preferences
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Email Notifications</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Send alerts via email
|
||||
</p>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="adminEmail">Admin Email</Label>
|
||||
<Input id="adminEmail" type="email" defaultValue="admin@ember-market.com" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Security Alerts</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Notify on security events
|
||||
</p>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>System Alerts</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Notify on system issues
|
||||
</p>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Order Alerts</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Notify on high-value orders
|
||||
</p>
|
||||
</div>
|
||||
<Switch />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Database Settings */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Database className="h-5 w-5 mr-2" />
|
||||
Database Settings
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Database configuration and maintenance
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="backupFrequency">Backup Frequency</Label>
|
||||
<Select defaultValue="daily">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="hourly">Hourly</SelectItem>
|
||||
<SelectItem value="daily">Daily</SelectItem>
|
||||
<SelectItem value="weekly">Weekly</SelectItem>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Auto Backup</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Automatically backup database
|
||||
</p>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="retentionPeriod">Backup Retention (days)</Label>
|
||||
<Input id="retentionPeriod" type="number" defaultValue="30" />
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Database Actions</Label>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm">
|
||||
Create Backup
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Optimize Database
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* API Settings */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Key className="h-5 w-5 mr-2" />
|
||||
API Settings
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
API configuration and rate limiting
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="apiVersion">API Version</Label>
|
||||
<Input id="apiVersion" defaultValue="v1" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="rateLimit">Rate Limit (requests/minute)</Label>
|
||||
<Input id="rateLimit" type="number" defaultValue="1000" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>API Logging</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Log all API requests
|
||||
</p>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="apiKey">Master API Key</Label>
|
||||
<Input id="apiKey" type="password" defaultValue="••••••••••••••••" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* System Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Globe className="h-5 w-5 mr-2" />
|
||||
System Information
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Platform version and system details
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm font-medium">Platform Version</span>
|
||||
<span className="text-sm text-muted-foreground">v2.1.0</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm font-medium">Database Version</span>
|
||||
<span className="text-sm text-muted-foreground">MongoDB 6.0</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm font-medium">Node.js Version</span>
|
||||
<span className="text-sm text-muted-foreground">v18.17.0</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm font-medium">Last Updated</span>
|
||||
<span className="text-sm text-muted-foreground">2024-01-15</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm font-medium">Uptime</span>
|
||||
<span className="text-sm text-muted-foreground">15 days, 3 hours</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end">
|
||||
<Button className="w-full sm:w-auto">
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save All Settings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
210
app/dashboard/admin/status/page.tsx
Normal file
210
app/dashboard/admin/status/page.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Server, Database, Cpu, HardDrive, Activity } from "lucide-react";
|
||||
import { fetchServer } from "@/lib/api";
|
||||
|
||||
interface SystemStatus {
|
||||
uptimeSeconds: number;
|
||||
memory: {
|
||||
rss: number;
|
||||
heapTotal: number;
|
||||
heapUsed: number;
|
||||
external: number;
|
||||
arrayBuffers: number;
|
||||
};
|
||||
versions: Record<string, string>;
|
||||
counts: {
|
||||
vendors: number;
|
||||
orders: number;
|
||||
products: number;
|
||||
chats: number;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function AdminStatusPage() {
|
||||
let systemStatus: SystemStatus | null = null;
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
systemStatus = await fetchServer<SystemStatus>("/admin/system-status");
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch system status:", err);
|
||||
error = "Failed to load system status";
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">System Status</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">Monitor system health and performance metrics</p>
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center text-red-500">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const formatUptime = (seconds: number) => {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return `${days}d ${hours}h ${minutes}m`;
|
||||
};
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const memoryUsagePercent = systemStatus ?
|
||||
Math.round((systemStatus.memory.heapUsed / systemStatus.memory.heapTotal) * 100) : 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">System Status</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">Monitor system health and performance metrics</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{/* Server Status */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Server Status</CardTitle>
|
||||
<Server className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="default" className="bg-green-500">Online</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{systemStatus ? formatUptime(systemStatus.uptimeSeconds) : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Last checked: {new Date().toLocaleTimeString()}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Database Status */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Database</CardTitle>
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="default" className="bg-green-500">Connected</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{systemStatus ? `${systemStatus.counts.vendors + systemStatus.counts.orders + systemStatus.counts.products} records` : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Total collections: 4
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Memory Usage */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Memory</CardTitle>
|
||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant={memoryUsagePercent > 80 ? "destructive" : memoryUsagePercent > 60 ? "secondary" : "outline"}>
|
||||
{memoryUsagePercent}%
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{systemStatus ? formatBytes(systemStatus.memory.heapUsed) : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Total: {systemStatus ? formatBytes(systemStatus.memory.heapTotal) : 'N/A'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Platform Stats */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Platform Stats</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="default" className="bg-green-500">Active</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{systemStatus ? `${systemStatus.counts.vendors} vendors` : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{systemStatus ? `${systemStatus.counts.orders} orders, ${systemStatus.counts.products} products` : 'N/A'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Node.js Version */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Runtime</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="outline">
|
||||
{systemStatus ? `Node ${systemStatus.versions.node}` : 'N/A'}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">Runtime</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{systemStatus ? `V8: ${systemStatus.versions.v8}` : 'N/A'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent System Activity</CardTitle>
|
||||
<CardDescription>Latest system events and changes</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">System health check completed</p>
|
||||
<p className="text-xs text-muted-foreground">2 minutes ago</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Database backup completed</p>
|
||||
<p className="text-xs text-muted-foreground">1 hour ago</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">High memory usage detected</p>
|
||||
<p className="text-xs text-muted-foreground">3 hours ago</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
188
app/dashboard/admin/vendors/page.tsx
vendored
Normal file
188
app/dashboard/admin/vendors/page.tsx
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
import React from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Search, MoreHorizontal, UserCheck, UserX, Mail } from "lucide-react";
|
||||
import { fetchServer } from "@/lib/api";
|
||||
|
||||
interface Vendor {
|
||||
_id: string;
|
||||
username: string;
|
||||
email?: string;
|
||||
storeId?: string;
|
||||
pgpKey?: string;
|
||||
createdAt?: string;
|
||||
currentToken?: string;
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
export default async function AdminVendorsPage() {
|
||||
let vendors: Vendor[] = [];
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
vendors = await fetchServer<Vendor[]>("/admin/vendors");
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch vendors:", err);
|
||||
error = "Failed to load vendors";
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">All Vendors</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">Manage vendor accounts and permissions</p>
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center text-red-500">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const activeVendors = vendors.filter(v => v.currentToken);
|
||||
const adminVendors = vendors.filter(v => v.isAdmin);
|
||||
const suspendedVendors = vendors.filter(v => !v.currentToken);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">All Vendors</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">Manage vendor accounts and permissions</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Vendors</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{vendors.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Registered vendors</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Active Vendors</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{activeVendors.length}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{vendors.length > 0 ? Math.round((activeVendors.length / vendors.length) * 100) : 0}% active rate
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Suspended</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{suspendedVendors.length}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{vendors.length > 0 ? Math.round((suspendedVendors.length / vendors.length) * 100) : 0}% suspension rate
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Admin Users</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{adminVendors.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Administrative access</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Vendor Management</CardTitle>
|
||||
<CardDescription>View and manage all vendor accounts</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input placeholder="Search vendors..." className="pl-8 w-64" />
|
||||
</div>
|
||||
<Button variant="outline" size="sm">
|
||||
<Mail className="h-4 w-4 mr-2" />
|
||||
Send Message
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Vendor</TableHead>
|
||||
<TableHead>Store</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Join Date</TableHead>
|
||||
<TableHead>Orders</TableHead>
|
||||
<TableHead>Revenue</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{vendors.map((vendor) => (
|
||||
<TableRow key={vendor._id}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className="font-medium">{vendor.username}</div>
|
||||
<div className="text-sm text-muted-foreground">{vendor.email || 'No email'}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{vendor.storeId || 'No store'}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Badge
|
||||
variant={vendor.currentToken ? "default" : "destructive"}
|
||||
>
|
||||
{vendor.currentToken ? "active" : "suspended"}
|
||||
</Badge>
|
||||
{vendor.isAdmin && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Admin
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{vendor.createdAt ? new Date(vendor.createdAt).toLocaleDateString() : 'N/A'}
|
||||
</TableCell>
|
||||
<TableCell>N/A</TableCell>
|
||||
<TableCell>N/A</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<UserCheck className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<UserX className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
214
components/admin/OrdersTable.tsx
Normal file
214
components/admin/OrdersTable.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Search, Filter, Eye, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
interface Order {
|
||||
orderId: string | number;
|
||||
userId: string;
|
||||
total: number;
|
||||
createdAt: string;
|
||||
status: string;
|
||||
items: Array<{
|
||||
name: string;
|
||||
quantity: number;
|
||||
}>;
|
||||
vendorUsername?: string;
|
||||
}
|
||||
|
||||
interface OrdersTableProps {
|
||||
orders: Order[];
|
||||
}
|
||||
|
||||
const getStatusStyle = (status: string) => {
|
||||
switch (status) {
|
||||
case 'acknowledged':
|
||||
return 'bg-purple-500/10 text-purple-500 border-purple-500/20';
|
||||
case 'paid':
|
||||
return 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20';
|
||||
case 'shipped':
|
||||
return 'bg-blue-500/10 text-blue-500 border-blue-500/20';
|
||||
case 'completed':
|
||||
return 'bg-green-500/10 text-green-500 border-green-500/20';
|
||||
case 'cancelled':
|
||||
return 'bg-red-500/10 text-red-500 border-red-500/20';
|
||||
case 'unpaid':
|
||||
return 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20';
|
||||
case 'confirming':
|
||||
return 'bg-orange-500/10 text-orange-500 border-orange-500/20';
|
||||
default:
|
||||
return 'bg-gray-500/10 text-gray-500 border-gray-500/20';
|
||||
}
|
||||
};
|
||||
|
||||
export default function OrdersTable({ orders }: OrdersTableProps) {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
const itemsPerPage = 10;
|
||||
|
||||
// Filter orders based on search and status
|
||||
const filteredOrders = orders.filter(order => {
|
||||
const matchesSearch = order.orderId.toString().toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
order.userId.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(order.vendorUsername && order.vendorUsername.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
|
||||
const matchesStatus = statusFilter === "all" || order.status === statusFilter;
|
||||
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
// Calculate pagination
|
||||
const totalPages = Math.ceil(filteredOrders.length / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const currentOrders = filteredOrders.slice(startIndex, endIndex);
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearchTerm(value);
|
||||
setCurrentPage(1); // Reset to first page when searching
|
||||
};
|
||||
|
||||
const handleStatusFilterChange = (value: string) => {
|
||||
setStatusFilter(value);
|
||||
setCurrentPage(1); // Reset to first page when filtering
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Order Management</CardTitle>
|
||||
<CardDescription>View and manage platform orders</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search orders..."
|
||||
className="pl-8 w-64"
|
||||
value={searchTerm}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Select value={statusFilter} onValueChange={handleStatusFilterChange}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Status</SelectItem>
|
||||
<SelectItem value="acknowledged">Acknowledged</SelectItem>
|
||||
<SelectItem value="paid">Paid</SelectItem>
|
||||
<SelectItem value="completed">Completed</SelectItem>
|
||||
<SelectItem value="cancelled">Cancelled</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="sm">
|
||||
<Filter className="h-4 w-4 mr-2" />
|
||||
Filters
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Order ID</TableHead>
|
||||
<TableHead>Customer</TableHead>
|
||||
<TableHead>Vendor</TableHead>
|
||||
<TableHead>Product</TableHead>
|
||||
<TableHead>Amount</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Payment</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{currentOrders.map((order) => (
|
||||
<TableRow key={order.orderId}>
|
||||
<TableCell className="font-medium">{order.orderId}</TableCell>
|
||||
<TableCell>{order.userId}</TableCell>
|
||||
<TableCell>{order.vendorUsername || 'N/A'}</TableCell>
|
||||
<TableCell className="max-w-[200px] truncate">
|
||||
{order.items.length > 0 ? order.items[0].name : 'No items'}
|
||||
</TableCell>
|
||||
<TableCell>£{order.total.toFixed(2)}</TableCell>
|
||||
<TableCell>
|
||||
<div className={`px-3 py-1 rounded-full border ${getStatusStyle(order.status)}`}>
|
||||
{order.status.toUpperCase()}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>N/A</TableCell>
|
||||
<TableCell>{new Date(order.createdAt).toLocaleDateString()}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="outline" size="sm">
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing {startIndex + 1} to {Math.min(endIndex, filteredOrders.length)} of {filteredOrders.length} orders
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||
const pageNum = i + 1;
|
||||
return (
|
||||
<Button
|
||||
key={pageNum}
|
||||
variant={currentPage === pageNum ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(pageNum)}
|
||||
className="w-8 h-8 p-0"
|
||||
>
|
||||
{pageNum}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -2,17 +2,43 @@
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ShoppingCart, LogOut } from "lucide-react"
|
||||
import { useRouter, usePathname } from "next/navigation"
|
||||
import { ShoppingCart, LogOut, Shield } from "lucide-react"
|
||||
import { NavItem } from "./nav-item"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { sidebarConfig } from "@/config/sidebar"
|
||||
import { adminSidebarConfig } from "@/config/admin-sidebar"
|
||||
import { logoutUser } from "@/lib/utils/auth"
|
||||
import { toast } from "sonner"
|
||||
import { useUser } from "@/hooks/useUser"
|
||||
|
||||
const Sidebar: React.FC = () => {
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { isAdmin } = useUser()
|
||||
|
||||
// Determine if we're in admin area
|
||||
const isAdminArea = pathname?.startsWith('/dashboard/admin')
|
||||
|
||||
// Filter sidebar config based on admin status
|
||||
const getFilteredConfig = () => {
|
||||
if (isAdminArea) {
|
||||
return adminSidebarConfig
|
||||
}
|
||||
|
||||
// Filter out admin section for non-admin users
|
||||
return sidebarConfig.filter(section => {
|
||||
if (section.title === "Administration") {
|
||||
return isAdmin
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
const currentConfig = getFilteredConfig()
|
||||
const homeLink = isAdminArea ? '/dashboard/admin' : '/dashboard'
|
||||
const icon = isAdminArea ? Shield : ShoppingCart
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
@@ -39,15 +65,15 @@ const Sidebar: React.FC = () => {
|
||||
`}
|
||||
>
|
||||
<div className="h-full flex flex-col">
|
||||
<Link href="/dashboard" className="h-16 px-6 flex items-center border-b border-border">
|
||||
<Link href={homeLink} className="h-16 px-6 flex items-center border-b border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<ShoppingCart className="h-6 w-6 text-foreground" />
|
||||
{icon === Shield ? <Shield className="h-6 w-6 text-foreground" /> : <ShoppingCart className="h-6 w-6 text-foreground" />}
|
||||
<span className="text-lg font-semibold text-foreground">Ember</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="flex-1 overflow-y-auto py-4 px-4 space-y-6">
|
||||
{sidebarConfig.map((section, index) => (
|
||||
{currentConfig.map((section, index) => (
|
||||
<div key={index}>
|
||||
<div className="px-3 mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{section.title}
|
||||
|
||||
27
config/admin-sidebar.ts
Normal file
27
config/admin-sidebar.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Home, Shield, Users, Ban, UserPlus, Package, Settings, BarChart3, AlertTriangle } from "lucide-react"
|
||||
|
||||
export const adminSidebarConfig = [
|
||||
{
|
||||
title: "Overview",
|
||||
items: [
|
||||
{ name: "Admin Dashboard", href: "/dashboard/admin", icon: Home },
|
||||
{ name: "System Status", href: "/dashboard/admin/status", icon: BarChart3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "User Management",
|
||||
items: [
|
||||
{ name: "All Vendors", href: "/dashboard/admin/vendors", icon: Users },
|
||||
{ name: "Invite Vendor", href: "/dashboard/admin/invite", icon: UserPlus },
|
||||
{ name: "Ban Users", href: "/dashboard/admin/ban", icon: Ban },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "System",
|
||||
items: [
|
||||
{ name: "Recent Orders", href: "/dashboard/admin/orders", icon: Package },
|
||||
{ name: "System Alerts", href: "/dashboard/admin/alerts", icon: AlertTriangle },
|
||||
{ name: "Settings", href: "/dashboard/admin/settings", icon: Settings },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Home, Package, Box, Truck, Settings, FolderTree, MessageCircle, BarChart3, Tag, Users, TrendingUp } from "lucide-react"
|
||||
import { Home, Package, Box, Truck, Settings, FolderTree, MessageCircle, BarChart3, Tag, Users, TrendingUp, Shield } from "lucide-react"
|
||||
|
||||
export const sidebarConfig = [
|
||||
{
|
||||
@@ -32,4 +32,10 @@ export const sidebarConfig = [
|
||||
{ name: "Storefront", href: "/dashboard/storefront", icon: Settings },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Administration",
|
||||
items: [
|
||||
{ name: "Admin Panel", href: "/dashboard/admin", icon: Shield },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
50
hooks/useUser.ts
Normal file
50
hooks/useUser.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { clientFetch } from '@/lib/api-client'
|
||||
|
||||
interface Vendor {
|
||||
_id: string;
|
||||
username: string;
|
||||
storeId: string;
|
||||
pgpKey: string;
|
||||
__v: number;
|
||||
}
|
||||
|
||||
interface User {
|
||||
vendor: Vendor;
|
||||
}
|
||||
|
||||
export function useUser() {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const userData = await clientFetch<User>("/auth/me")
|
||||
setUser(userData)
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch user:", err)
|
||||
setError("Failed to fetch user data")
|
||||
setUser(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchUser()
|
||||
}, [])
|
||||
|
||||
const isAdmin = user?.vendor?.username === 'admin1'
|
||||
|
||||
return {
|
||||
user,
|
||||
loading,
|
||||
error,
|
||||
isAdmin
|
||||
}
|
||||
}
|
||||
@@ -61,13 +61,13 @@ export async function middleware(req: NextRequest) {
|
||||
|
||||
console.log("Middleware: Auth check successful");
|
||||
|
||||
// Admin-only protection for /admin routes
|
||||
if (pathname.startsWith('/admin')) {
|
||||
// Admin-only protection for /dashboard/admin routes
|
||||
if (pathname.startsWith('/dashboard/admin')) {
|
||||
try {
|
||||
const user = await res.json();
|
||||
const username = user?.vendor?.username;
|
||||
if (username !== 'admin1') {
|
||||
console.log("Middleware: Non-admin attempted to access /admin, redirecting");
|
||||
console.log("Middleware: Non-admin attempted to access /dashboard/admin, redirecting");
|
||||
return NextResponse.redirect(new URL("/dashboard", req.url));
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -84,5 +84,5 @@ export async function middleware(req: NextRequest) {
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/dashboard/:path*", "/admin/:path*", "/auth/reset-password/:path*"],
|
||||
matcher: ["/dashboard/:path*", "/auth/reset-password/:path*"],
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"commitHash": "577c93d",
|
||||
"buildTime": "2025-10-16T11:12:16.746Z"
|
||||
"commitHash": "03a2e37",
|
||||
"buildTime": "2025-10-16T20:09:52.215Z"
|
||||
}
|
||||
Reference in New Issue
Block a user