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";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
import InviteVendorCard from "@/components/admin/InviteVendorCard";
|
import InviteVendorCard from "@/components/admin/InviteVendorCard";
|
||||||
import BanUserCard from "@/components/admin/BanUserCard";
|
import BanUserCard from "@/components/admin/BanUserCard";
|
||||||
import RecentOrdersCard from "@/components/admin/RecentOrdersCard";
|
import RecentOrdersCard from "@/components/admin/RecentOrdersCard";
|
||||||
@@ -9,7 +10,7 @@ import VendorsCard from "@/components/admin/VendorsCard";
|
|||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">Admin</h1>
|
<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>
|
<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 { useState } from "react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter, usePathname } from "next/navigation"
|
||||||
import { ShoppingCart, LogOut } from "lucide-react"
|
import { ShoppingCart, LogOut, Shield } from "lucide-react"
|
||||||
import { NavItem } from "./nav-item"
|
import { NavItem } from "./nav-item"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { sidebarConfig } from "@/config/sidebar"
|
import { sidebarConfig } from "@/config/sidebar"
|
||||||
|
import { adminSidebarConfig } from "@/config/admin-sidebar"
|
||||||
import { logoutUser } from "@/lib/utils/auth"
|
import { logoutUser } from "@/lib/utils/auth"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
import { useUser } from "@/hooks/useUser"
|
||||||
|
|
||||||
const Sidebar: React.FC = () => {
|
const Sidebar: React.FC = () => {
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||||
const router = useRouter()
|
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 () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -39,15 +65,15 @@ const Sidebar: React.FC = () => {
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div className="h-full flex flex-col">
|
<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">
|
<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>
|
<span className="text-lg font-semibold text-foreground">Ember</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto py-4 px-4 space-y-6">
|
<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 key={index}>
|
||||||
<div className="px-3 mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
<div className="px-3 mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||||
{section.title}
|
{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 = [
|
export const sidebarConfig = [
|
||||||
{
|
{
|
||||||
@@ -32,4 +32,10 @@ export const sidebarConfig = [
|
|||||||
{ name: "Storefront", href: "/dashboard/storefront", icon: Settings },
|
{ 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");
|
console.log("Middleware: Auth check successful");
|
||||||
|
|
||||||
// Admin-only protection for /admin routes
|
// Admin-only protection for /dashboard/admin routes
|
||||||
if (pathname.startsWith('/admin')) {
|
if (pathname.startsWith('/dashboard/admin')) {
|
||||||
try {
|
try {
|
||||||
const user = await res.json();
|
const user = await res.json();
|
||||||
const username = user?.vendor?.username;
|
const username = user?.vendor?.username;
|
||||||
if (username !== 'admin1') {
|
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));
|
return NextResponse.redirect(new URL("/dashboard", req.url));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -84,5 +84,5 @@ export async function middleware(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
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",
|
"commitHash": "03a2e37",
|
||||||
"buildTime": "2025-10-16T11:12:16.746Z"
|
"buildTime": "2025-10-16T20:09:52.215Z"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user