feat(dashboard): real data — metrics, charts, top customers, recent transactions
Dashboard is no longer mock data. Single getDashboardData(tenantId) server
query computes everything in one pass.
New aggregator (lib/appwrite/dashboard-queries.ts):
- Pulls customers, invoices, finance_entries, tasks, services in parallel.
- Derives:
* metrics: totalCustomers, activeCustomers, monthIncome,
prevMonthIncome (for delta), outstanding (unpaid invoice total),
overdueCount, openTasks, urgentTasks
* monthlyIncome: 12-month income+expense series for area chart
* topCustomers: 5 highest-grossing customers by paid invoice total
* recentTransactions: 8 newest finance entries
* topServices: 5 services by aggregate unit price (placeholder, will
refine when we have invoice line analytics)
* newCustomersMonthly: 6-month new customer count for bar chart
Components (dashboard/components/):
- Metrics: 4 cards with trend indicator on income (delta vs previous
month), warning tone on overdue invoices and urgent tasks.
- IncomeChart: Recharts Area chart, dual income/expense series with
gradient fills, Turkish month labels.
- TopCustomers: ranked list with progress bars relative to top earner.
- RecentTransactions: list with type badge, signed amount, link to
/finance for full list.
- CustomerGrowth: BarChart of new customers per month (last 6).
- QuickActions: 4 buttons linking to /customers, /invoices, /calendar,
/tasks (replaced template's New User/Add Product/etc).
Layout: 4 metric cards row, then income chart + top customers (2-col),
then recent transactions + customer growth (2-col).
Removed:
- src/app/(dashboard)/dashboard-2/ (was the demo page; same components
re-exported into the real /dashboard from there. Now /dashboard owns
its components.)
- 'Dashboard 2' entry from CommandSearch; replaced with our actual
module list (Müşteriler / Hizmetler / Yazılımlarımız / Takvim /
Görevler / Gelir-Gider / Faturalar).
This commit is contained in:
@@ -1,248 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid } from "recharts"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Users, MapPin, TrendingUp, Target, ArrowUpIcon, UserIcon } from "lucide-react"
|
||||
|
||||
const customerGrowthData = [
|
||||
{ month: "Jan", new: 245, returning: 890, churn: 45 },
|
||||
{ month: "Feb", new: 312, returning: 934, churn: 52 },
|
||||
{ month: "Mar", new: 289, returning: 1023, churn: 38 },
|
||||
{ month: "Apr", new: 456, returning: 1156, churn: 61 },
|
||||
{ month: "May", new: 523, returning: 1298, churn: 47 },
|
||||
{ month: "Jun", new: 634, returning: 1445, churn: 55 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
new: {
|
||||
label: "New Customers",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
returning: {
|
||||
label: "Returning",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
churn: {
|
||||
label: "Churned",
|
||||
color: "var(--chart-3)",
|
||||
},
|
||||
}
|
||||
|
||||
const demographicsData = [
|
||||
{ ageGroup: "18-24", customers: 2847, percentage: "18.0%", growth: "+15.2%", growthColor: "text-green-600" },
|
||||
{ ageGroup: "25-34", customers: 4521, percentage: "28.5%", growth: "+8.7%", growthColor: "text-green-600" },
|
||||
{ ageGroup: "35-44", customers: 3982, percentage: "25.1%", growth: "+3.4%", growthColor: "text-blue-600" },
|
||||
{ ageGroup: "45-54", customers: 2734, percentage: "17.2%", growth: "+1.2%", growthColor: "text-orange-600" },
|
||||
{ ageGroup: "55+", customers: 1763, percentage: "11.2%", growth: "-2.1%", growthColor: "text-red-600" },
|
||||
]
|
||||
|
||||
const regionsData = [
|
||||
{ region: "North America", customers: 6847, revenue: "$847,523", growth: "+12.3%", growthColor: "text-green-600" },
|
||||
{ region: "Europe", customers: 4521, revenue: "$563,891", growth: "+9.7%", growthColor: "text-green-600" },
|
||||
{ region: "Asia Pacific", customers: 2892, revenue: "$321,456", growth: "+18.4%", growthColor: "text-blue-600" },
|
||||
{ region: "Latin America", customers: 1123, revenue: "$187,234", growth: "+15.8%", growthColor: "text-green-600" },
|
||||
{ region: "Others", customers: 464, revenue: "$67,891", growth: "+5.2%", growthColor: "text-orange-600" },
|
||||
]
|
||||
|
||||
export function CustomerInsights() {
|
||||
const [activeTab, setActiveTab] = useState("growth")
|
||||
|
||||
return (
|
||||
<Card className="h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle>Customer Insights</CardTitle>
|
||||
<CardDescription>Growth trends and demographics</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3 bg-muted/50 p-1 rounded-lg h-12">
|
||||
<TabsTrigger
|
||||
value="growth"
|
||||
className="cursor-pointer flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-all data-[state=active]:bg-background data-[state=active]:shadow-sm data-[state=active]:text-foreground"
|
||||
>
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Growth</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="demographics"
|
||||
className="cursor-pointer flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-all data-[state=active]:bg-background data-[state=active]:shadow-sm data-[state=active]:text-foreground"
|
||||
>
|
||||
<UserIcon className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Demographics</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="regions"
|
||||
className="cursor-pointer flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-all data-[state=active]:bg-background data-[state=active]:shadow-sm data-[state=active]:text-foreground"
|
||||
>
|
||||
<MapPin className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Regions</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="growth" className="mt-8 space-y-6">
|
||||
<div className="grid gap-6">
|
||||
{/* Chart and Key Metrics Side by Side */}
|
||||
<div className="grid grid-cols-10 gap-6">
|
||||
{/* Chart Area - 70% */}
|
||||
<div className="col-span-10 xl:col-span-7">
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-6">Customer Growth Trends</h3>
|
||||
<ChartContainer config={chartConfig} className="h-[375px] w-full">
|
||||
<BarChart data={customerGrowthData} margin={{ top: 20, right: 20, bottom: 20, left: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
className="text-xs"
|
||||
tick={{ fontSize: 12 }}
|
||||
tickLine={{ stroke: 'var(--border)' }}
|
||||
axisLine={{ stroke: 'var(--border)' }}
|
||||
/>
|
||||
<YAxis
|
||||
className="text-xs"
|
||||
tick={{ fontSize: 12 }}
|
||||
tickLine={{ stroke: 'var(--border)' }}
|
||||
axisLine={{ stroke: 'var(--border)' }}
|
||||
domain={[0, 'dataMax']}
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="new" fill="var(--color-new)" radius={[2, 2, 0, 0]} />
|
||||
<Bar dataKey="returning" fill="var(--color-returning)" radius={[2, 2, 0, 0]} />
|
||||
<Bar dataKey="churn" fill="var(--color-churn)" radius={[2, 2, 0, 0]} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
|
||||
{/* Key Metrics - 30% */}
|
||||
<div className="col-span-10 xl:col-span-3 space-y-5">
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-6">Key Metrics</h3>
|
||||
<div className="grid grid-cols-3 gap-5">
|
||||
<div className="p-4 rounded-lg max-lg:col-span-3 xl:col-span-3 border">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<TrendingUp className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm font-medium">Total Customers</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">15,847</div>
|
||||
<div className="text-xs text-green-600 flex items-center gap-1 mt-1">
|
||||
<ArrowUpIcon className="h-3 w-3" />
|
||||
+12.5% from last month
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg max-lg:col-span-3 xl:col-span-3 border">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Retention Rate</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">92.4%</div>
|
||||
<div className="text-xs text-green-600 flex items-center gap-1 mt-1">
|
||||
<ArrowUpIcon className="h-3 w-3" />
|
||||
+2.1% improvement
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg max-lg:col-span-3 xl:col-span-3 border">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Target className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Avg. LTV</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">$2,847</div>
|
||||
<div className="text-xs text-green-600 flex items-center gap-1 mt-1">
|
||||
<ArrowUpIcon className="h-3 w-3" />
|
||||
+8.3% growth
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="demographics" className="mt-8">
|
||||
<div className="rounded-lg border bg-card">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-b">
|
||||
<TableHead className="py-5 px-6 font-semibold">Age Group</TableHead>
|
||||
<TableHead className="text-right py-5 px-6 font-semibold">Customers</TableHead>
|
||||
<TableHead className="text-right py-5 px-6 font-semibold">Percentage</TableHead>
|
||||
<TableHead className="text-right py-5 px-6 font-semibold">Growth</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{demographicsData.map((row, index) => (
|
||||
<TableRow key={index} className="hover:bg-muted/30 transition-colors">
|
||||
<TableCell className="font-medium py-5 px-6">{row.ageGroup}</TableCell>
|
||||
<TableCell className="text-right py-5 px-6">{row.customers.toLocaleString()}</TableCell>
|
||||
<TableCell className="text-right py-5 px-6">{row.percentage}</TableCell>
|
||||
<TableCell className="text-right py-5 px-6">
|
||||
<span className={`font-medium ${row.growthColor}`}>{row.growth}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2 py-6">
|
||||
<div className="text-muted-foreground text-sm hidden sm:block">
|
||||
0 of {demographicsData.length} row(s) selected.
|
||||
</div>
|
||||
<div className="space-x-2 space-y-2">
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
Previous
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
<TabsContent value="regions" className="mt-8">
|
||||
<div className="rounded-lg border bg-card">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-b">
|
||||
<TableHead className="py-5 px-6 font-semibold">Region</TableHead>
|
||||
<TableHead className="text-right py-5 px-6 font-semibold">Customers</TableHead>
|
||||
<TableHead className="text-right py-5 px-6 font-semibold">Revenue</TableHead>
|
||||
<TableHead className="text-right py-5 px-6 font-semibold">Growth</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{regionsData.map((row, index) => (
|
||||
<TableRow key={index} className="hover:bg-muted/30 transition-colors">
|
||||
<TableCell className="font-medium py-5 px-6">{row.region}</TableCell>
|
||||
<TableCell className="text-right py-5 px-6">{row.customers.toLocaleString()}</TableCell>
|
||||
<TableCell className="text-right py-5 px-6">{row.revenue}</TableCell>
|
||||
<TableCell className="text-right py-5 px-6">
|
||||
<span className={`font-medium ${row.growthColor}`}>{row.growth}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2 py-6">
|
||||
<div className="text-muted-foreground text-sm hidden sm:block">
|
||||
0 of {regionsData.length} row(s) selected.
|
||||
</div>
|
||||
<div className="space-x-2 space-y-2">
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
Previous
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Calendar, Clock, RefreshCw, Filter } from "lucide-react"
|
||||
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
export function DashboardHeader() {
|
||||
const [dateRange, setDateRange] = useState("30d")
|
||||
const lastUpdated = new Date().toLocaleString()
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-3xl font-bold">Business Dashboard</CardTitle>
|
||||
<CardDescription className="text-base mt-2">
|
||||
Comprehensive overview of your business performance and key metrics
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="outline" className="cursor-pointer">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
Live Data
|
||||
</Badge>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer">
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">Date Range:</span>
|
||||
<Select value={dateRange} onValueChange={setDateRange}>
|
||||
<SelectTrigger className="w-40 cursor-pointer">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="7d" className="cursor-pointer">Last 7 days</SelectItem>
|
||||
<SelectItem value="30d" className="cursor-pointer">Last 30 days</SelectItem>
|
||||
<SelectItem value="90d" className="cursor-pointer">Last 90 days</SelectItem>
|
||||
<SelectItem value="1y" className="cursor-pointer">Last year</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer">
|
||||
<Filter className="h-4 w-4 mr-2" />
|
||||
Filters
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Last updated: {lastUpdated}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
DollarSign,
|
||||
Users,
|
||||
ShoppingCart,
|
||||
BarChart3
|
||||
} from "lucide-react"
|
||||
import { Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
const metrics = [
|
||||
{
|
||||
title: "Total Revenue",
|
||||
value: "$54,230",
|
||||
description: "Monthly revenue",
|
||||
change: "+12%",
|
||||
trend: "up",
|
||||
icon: DollarSign,
|
||||
footer: "Trending up this month",
|
||||
subfooter: "Revenue for the last 6 months"
|
||||
},
|
||||
{
|
||||
title: "Active Customers",
|
||||
value: "2,350",
|
||||
description: "Total active users",
|
||||
change: "+5.2%",
|
||||
trend: "up",
|
||||
icon: Users,
|
||||
footer: "Strong user retention",
|
||||
subfooter: "Engagement exceeds targets"
|
||||
},
|
||||
{
|
||||
title: "Total Orders",
|
||||
value: "1,247",
|
||||
description: "Orders this month",
|
||||
change: "-2.1%",
|
||||
trend: "down",
|
||||
icon: ShoppingCart,
|
||||
footer: "Down 2% this period",
|
||||
subfooter: "Order volume needs attention"
|
||||
},
|
||||
{
|
||||
title: "Conversion Rate",
|
||||
value: "3.24%",
|
||||
description: "Average conversion",
|
||||
change: "+8.3%",
|
||||
trend: "up",
|
||||
icon: BarChart3,
|
||||
footer: "Steady performance increase",
|
||||
subfooter: "Meets conversion projections"
|
||||
},
|
||||
]
|
||||
|
||||
export function MetricsOverview() {
|
||||
return (
|
||||
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs grid gap-4 sm:grid-cols-2 @5xl:grid-cols-4">
|
||||
{metrics.map((metric) => {
|
||||
const TrendIcon = metric.trend === "up" ? TrendingUp : TrendingDown
|
||||
|
||||
return (
|
||||
<Card key={metric.title} className=" cursor-pointer">
|
||||
<CardHeader>
|
||||
<CardDescription>{metric.title}</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
{metric.value}
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
<TrendIcon className="h-4 w-4" />
|
||||
{metric.change}
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
{metric.footer} <TrendIcon className="size-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
{metric.subfooter}
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Plus, Settings, FileText, Download } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
|
||||
export function QuickActions() {
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button className="cursor-pointer">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
New Sale
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="cursor-pointer">
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
Actions
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem className="cursor-pointer">
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
Generate Report
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer">
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Export Data
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="cursor-pointer">
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
Dashboard Settings
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Eye, MoreHorizontal } from "lucide-react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
|
||||
const transactions = [
|
||||
{
|
||||
id: "TXN-001",
|
||||
customer: {
|
||||
name: "Olivia Martin",
|
||||
email: "olivia.martin@email.com",
|
||||
avatar: "https://notion-avatars.netlify.app/api/avatar/?preset=female-7",
|
||||
},
|
||||
amount: "$1,999.00",
|
||||
status: "completed",
|
||||
date: "2 hours ago",
|
||||
},
|
||||
{
|
||||
id: "TXN-002",
|
||||
customer: {
|
||||
name: "Jackson Lee",
|
||||
email: "jackson.lee@email.com",
|
||||
avatar: "https://notion-avatars.netlify.app/api/avatar/?preset=male-1",
|
||||
},
|
||||
amount: "$2,999.00",
|
||||
status: "pending",
|
||||
date: "5 hours ago",
|
||||
},
|
||||
{
|
||||
id: "TXN-003",
|
||||
customer: {
|
||||
name: "Isabella Nguyen",
|
||||
email: "isabella.nguyen@email.com",
|
||||
avatar: "https://notion-avatars.netlify.app/api/avatar/?preset=female-2",
|
||||
},
|
||||
amount: "$39.00",
|
||||
status: "completed",
|
||||
date: "1 day ago",
|
||||
},
|
||||
{
|
||||
id: "TXN-004",
|
||||
customer: {
|
||||
name: "William Kim",
|
||||
email: "will@email.com",
|
||||
avatar: "https://notion-avatars.netlify.app/api/avatar/?preset=male-5",
|
||||
},
|
||||
amount: "$299.00",
|
||||
status: "failed",
|
||||
date: "2 days ago",
|
||||
},
|
||||
{
|
||||
id: "TXN-005",
|
||||
customer: {
|
||||
name: "Sofia Davis",
|
||||
email: "sofia.davis@email.com",
|
||||
avatar: "https://notion-avatars.netlify.app/api/avatar/?preset=female-4",
|
||||
},
|
||||
amount: "$99.00",
|
||||
status: "completed",
|
||||
date: "3 days ago",
|
||||
},
|
||||
]
|
||||
|
||||
export function RecentTransactions() {
|
||||
return (
|
||||
<Card className="cursor-pointer">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
||||
<div>
|
||||
<CardTitle>Recent Transactions</CardTitle>
|
||||
<CardDescription>Latest customer transactions</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer">
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
View All
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{transactions.map((transaction) => (
|
||||
<div key={transaction.id} >
|
||||
<div className="flex p-3 rounded-lg border gap-2">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={transaction.customer.avatar} alt={transaction.customer.name} />
|
||||
<AvatarFallback>{transaction.customer.name.split(" ").map(n => n[0]).join("")}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-1 items-center flex-wrap justify-between gap-1">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium truncate">{transaction.customer.name}</p>
|
||||
<p className="text-xs text-muted-foreground truncate">{transaction.customer.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Badge
|
||||
variant={
|
||||
transaction.status === "completed" ? "default" :
|
||||
transaction.status === "pending" ? "secondary" : "destructive"
|
||||
}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{transaction.status}
|
||||
</Badge>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium">{transaction.amount}</p>
|
||||
<p className="text-xs text-muted-foreground">{transaction.date}</p>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 cursor-pointer">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem className="cursor-pointer">View Details</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer">Download Receipt</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer">Contact Customer</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Label, Pie, PieChart, Sector } from "recharts"
|
||||
import type { PieSectorDataItem } from "recharts/types/polar/Pie"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ChartContainer, ChartStyle, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
const revenueData = [
|
||||
{ category: "subscriptions", value: 45, amount: 24500, fill: "var(--color-subscriptions)" },
|
||||
{ category: "sales", value: 30, amount: 16300, fill: "var(--color-sales)" },
|
||||
{ category: "services", value: 15, amount: 8150, fill: "var(--color-services)" },
|
||||
{ category: "partnerships", value: 10, amount: 5430, fill: "var(--color-partnerships)" },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
revenue: {
|
||||
label: "Revenue",
|
||||
},
|
||||
amount: {
|
||||
label: "Amount",
|
||||
},
|
||||
subscriptions: {
|
||||
label: "Subscriptions",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
sales: {
|
||||
label: "One-time Sales",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
services: {
|
||||
label: "Services",
|
||||
color: "var(--chart-3)",
|
||||
},
|
||||
partnerships: {
|
||||
label: "Partnerships",
|
||||
color: "var(--chart-4)",
|
||||
},
|
||||
}
|
||||
|
||||
export function RevenueBreakdown() {
|
||||
const id = "revenue-breakdown"
|
||||
const [activeCategory, setActiveCategory] = React.useState("sales")
|
||||
|
||||
const activeIndex = React.useMemo(() => {
|
||||
const index = revenueData.findIndex((item) => item.category === activeCategory)
|
||||
return index === -1 ? 0 : index
|
||||
}, [activeCategory])
|
||||
|
||||
const categories = React.useMemo(() => revenueData.map((item) => item.category), [])
|
||||
|
||||
return (
|
||||
<Card data-chart={id} className="flex flex-col cursor-pointer">
|
||||
<ChartStyle id={id} config={chartConfig} />
|
||||
<CardHeader className="flex flex-col space-y-2 sm:flex-row sm:items-center sm:justify-between sm:space-y-0 pb-2">
|
||||
<div>
|
||||
<CardTitle>Revenue Breakdown</CardTitle>
|
||||
<CardDescription>Revenue distribution by source</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Select value={activeCategory} onValueChange={setActiveCategory}>
|
||||
<SelectTrigger
|
||||
className="w-[175px] rounded-lg cursor-pointer"
|
||||
aria-label="Select a category"
|
||||
>
|
||||
<SelectValue placeholder="Select category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end" className="rounded-lg">
|
||||
{categories.map((key) => {
|
||||
const config = chartConfig[key as keyof typeof chartConfig]
|
||||
|
||||
if (!config) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectItem
|
||||
key={key}
|
||||
value={key}
|
||||
className="rounded-md [&_span]:flex cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="flex h-3 w-3 shrink-0 "
|
||||
style={{
|
||||
backgroundColor: `var(--color-${key})`,
|
||||
}}
|
||||
/>
|
||||
{config?.label}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" className="cursor-pointer">
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 justify-center">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
|
||||
<div className="flex justify-center">
|
||||
<ChartContainer
|
||||
id={id}
|
||||
config={chartConfig}
|
||||
className="mx-auto aspect-square w-full max-w-[300px]"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Pie
|
||||
data={revenueData}
|
||||
dataKey="amount"
|
||||
nameKey="category"
|
||||
innerRadius={60}
|
||||
strokeWidth={5}
|
||||
activeShape={({
|
||||
outerRadius = 0,
|
||||
...props
|
||||
}: PieSectorDataItem) => (
|
||||
<g>
|
||||
<Sector {...props} outerRadius={outerRadius + 10} />
|
||||
<Sector
|
||||
{...props}
|
||||
outerRadius={outerRadius + 25}
|
||||
innerRadius={outerRadius + 12}
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
className="fill-foreground text-3xl font-bold"
|
||||
>
|
||||
${(revenueData[activeIndex].amount / 1000).toFixed(0)}K
|
||||
</tspan>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) + 24}
|
||||
className="fill-muted-foreground"
|
||||
>
|
||||
Revenue
|
||||
</tspan>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center space-y-4">
|
||||
{revenueData.map((item, index) => {
|
||||
const config = chartConfig[item.category as keyof typeof chartConfig]
|
||||
const isActive = index === activeIndex
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.category}
|
||||
className={`flex items-center justify-between p-3 rounded-lg transition-colors cursor-pointer ${
|
||||
isActive ? 'bg-muted' : 'hover:bg-muted/50'
|
||||
}`}
|
||||
onClick={() => setActiveCategory(item.category)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className="flex h-3 w-3 shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: `var(--color-${item.category})`,
|
||||
}}
|
||||
/>
|
||||
<span className="font-medium">{config?.label}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-bold">${(item.amount / 1000).toFixed(1)}K</div>
|
||||
<div className="text-sm text-muted-foreground">{item.value}%</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
const salesData = [
|
||||
{ month: "Jan", sales: 12500, target: 15000 },
|
||||
{ month: "Feb", sales: 18200, target: 15000 },
|
||||
{ month: "Mar", sales: 16800, target: 15000 },
|
||||
{ month: "Apr", sales: 22400, target: 20000 },
|
||||
{ month: "May", sales: 24600, target: 20000 },
|
||||
{ month: "Jun", sales: 28200, target: 25000 },
|
||||
{ month: "Jul", sales: 31500, target: 25000 },
|
||||
{ month: "Aug", sales: 29800, target: 25000 },
|
||||
{ month: "Sep", sales: 33200, target: 30000 },
|
||||
{ month: "Oct", sales: 35100, target: 30000 },
|
||||
{ month: "Nov", sales: 38900, target: 35000 },
|
||||
{ month: "Dec", sales: 42300, target: 35000 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
sales: {
|
||||
label: "Sales",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
target: {
|
||||
label: "Target",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
}
|
||||
|
||||
export function SalesChart() {
|
||||
const [timeRange, setTimeRange] = useState("12m")
|
||||
|
||||
return (
|
||||
<Card className="cursor-pointer">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<div>
|
||||
<CardTitle>Sales Performance</CardTitle>
|
||||
<CardDescription>Monthly sales vs targets</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||
<SelectTrigger className="w-32 cursor-pointer">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="3m" className="cursor-pointer">Last 3 months</SelectItem>
|
||||
<SelectItem value="6m" className="cursor-pointer">Last 6 months</SelectItem>
|
||||
<SelectItem value="12m" className="cursor-pointer">Last 12 months</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" className="cursor-pointer">
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0 pt-6">
|
||||
<div className="px-6 pb-6">
|
||||
<ChartContainer config={chartConfig} className="h-[350px] w-full">
|
||||
<AreaChart data={salesData} margin={{ top: 10, right: 10, left: 10, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorSales" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--color-sales)" stopOpacity={0.4} />
|
||||
<stop offset="95%" stopColor="var(--color-sales)" stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
<linearGradient id="colorTarget" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--color-target)" stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor="var(--color-target)" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted/30" />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs"
|
||||
tick={{ fontSize: 12 }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs"
|
||||
tick={{ fontSize: 12 }}
|
||||
tickFormatter={(value) => `$${value.toLocaleString()}`}
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="target"
|
||||
stackId="1"
|
||||
stroke="var(--color-target)"
|
||||
fill="url(#colorTarget)"
|
||||
strokeDasharray="5 5"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="sales"
|
||||
stackId="2"
|
||||
stroke="var(--color-sales)"
|
||||
fill="url(#colorSales)"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Eye, Star, TrendingUp } from "lucide-react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
|
||||
const products = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Premium Dashboard",
|
||||
sales: 2847,
|
||||
revenue: "$142,350",
|
||||
growth: "+23%",
|
||||
rating: 4.8,
|
||||
stock: 145,
|
||||
category: "Software",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Analytics Pro",
|
||||
sales: 1923,
|
||||
revenue: "$96,150",
|
||||
growth: "+18%",
|
||||
rating: 4.6,
|
||||
stock: 67,
|
||||
category: "Tools",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Mobile App Suite",
|
||||
sales: 1456,
|
||||
revenue: "$72,800",
|
||||
growth: "+12%",
|
||||
rating: 4.9,
|
||||
stock: 234,
|
||||
category: "Mobile",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Enterprise License",
|
||||
sales: 892,
|
||||
revenue: "$178,400",
|
||||
growth: "+8%",
|
||||
rating: 4.7,
|
||||
stock: 12,
|
||||
category: "Enterprise",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Basic Subscription",
|
||||
sales: 3421,
|
||||
revenue: "$68,420",
|
||||
growth: "+31%",
|
||||
rating: 4.4,
|
||||
stock: 999,
|
||||
category: "Subscription",
|
||||
},
|
||||
]
|
||||
|
||||
export function TopProducts() {
|
||||
return (
|
||||
<Card className="cursor-pointer">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
||||
<div>
|
||||
<CardTitle>Top Products</CardTitle>
|
||||
<CardDescription>Best performing products this month</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer">
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
View All
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{products.map((product, index) => (
|
||||
<div key={product.id} className="flex items-center p-3 rounded-lg border gap-2">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-semibold text-sm">
|
||||
#{index + 1}
|
||||
</div>
|
||||
<div className="flex gap-2 items-center justify-between space-x-3 flex-1 flex-wrap">
|
||||
<div className="">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm font-medium truncate">{product.name}</p>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{product.category}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Star className="h-3 w-3 fill-yellow-400 text-yellow-400" />
|
||||
<span className="text-xs text-muted-foreground">{product.rating}</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">•</span>
|
||||
<span className="text-xs text-muted-foreground">{product.sales} sales</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm font-medium">{product.revenue}</p>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-green-600 border-green-200 cursor-pointer"
|
||||
>
|
||||
<TrendingUp className="h-3 w-3 mr-1" />
|
||||
{product.growth}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-muted-foreground">Stock: {product.stock}</span>
|
||||
<Progress
|
||||
value={product.stock > 100 ? 100 : (product.stock / 100) * 100}
|
||||
className="w-12 h-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"totalRevenue": 54231.89,
|
||||
"revenueChange": 12.5,
|
||||
"activeCustomers": 2350,
|
||||
"customerChange": 5.2,
|
||||
"totalOrders": 1247,
|
||||
"orderChange": -2.1,
|
||||
"conversionRate": 3.24,
|
||||
"conversionChange": 8.3,
|
||||
"salesData": [
|
||||
{ "month": "Jan", "sales": 12500, "target": 15000 },
|
||||
{ "month": "Feb", "sales": 18200, "target": 15000 },
|
||||
{ "month": "Mar", "sales": 16800, "target": 15000 },
|
||||
{ "month": "Apr", "sales": 22400, "target": 20000 },
|
||||
{ "month": "May", "sales": 24600, "target": 20000 },
|
||||
{ "month": "Jun", "sales": 28200, "target": 25000 },
|
||||
{ "month": "Jul", "sales": 31500, "target": 25000 },
|
||||
{ "month": "Aug", "sales": 29800, "target": 25000 },
|
||||
{ "month": "Sep", "sales": 33200, "target": 30000 },
|
||||
{ "month": "Oct", "sales": 35100, "target": 30000 },
|
||||
{ "month": "Nov", "sales": 38900, "target": 35000 },
|
||||
{ "month": "Dec", "sales": 42300, "target": 35000 }
|
||||
],
|
||||
"revenueBreakdown": [
|
||||
{ "name": "Subscriptions", "value": 45, "amount": 24500, "color": "hsl(210, 100%, 50%)" },
|
||||
{ "name": "One-time Sales", "value": 30, "amount": 16300, "color": "hsl(280, 100%, 70%)" },
|
||||
{ "name": "Services", "value": 15, "amount": 8150, "color": "hsl(120, 100%, 40%)" },
|
||||
{ "name": "Partnerships", "value": 10, "amount": 5430, "color": "hsl(30, 100%, 50%)" }
|
||||
],
|
||||
"customerGrowth": [
|
||||
{ "month": "Jan", "new": 245, "returning": 890, "churn": 45 },
|
||||
{ "month": "Feb", "new": 312, "returning": 934, "churn": 52 },
|
||||
{ "month": "Mar", "new": 289, "returning": 1023, "churn": 38 },
|
||||
{ "month": "Apr", "new": 456, "returning": 1156, "churn": 61 },
|
||||
{ "month": "May", "new": 523, "returning": 1298, "churn": 47 },
|
||||
{ "month": "Jun", "new": 634, "returning": 1445, "churn": 55 }
|
||||
],
|
||||
"lastUpdated": "2025-08-12T15:30:00Z"
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { MetricsOverview } from "./components/metrics-overview"
|
||||
import { SalesChart } from "./components/sales-chart"
|
||||
import { RecentTransactions } from "./components/recent-transactions"
|
||||
import { TopProducts } from "./components/top-products"
|
||||
import { CustomerInsights } from "./components/customer-insights"
|
||||
import { QuickActions } from "./components/quick-actions"
|
||||
import { RevenueBreakdown } from "./components/revenue-breakdown"
|
||||
|
||||
export default function Dashboard2() {
|
||||
return (
|
||||
<div className="flex-1 space-y-6 px-6 pt-0">
|
||||
{/* Enhanced Header */}
|
||||
|
||||
<div className="flex md:flex-row flex-col md:items-center justify-between gap-4 md:gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Business Dashboard</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Monitor your business performance and key metrics in real-time
|
||||
</p>
|
||||
</div>
|
||||
<QuickActions />
|
||||
</div>
|
||||
|
||||
{/* Main Dashboard Grid */}
|
||||
<div className="@container/main space-y-6">
|
||||
{/* Top Row - Key Metrics */}
|
||||
|
||||
<MetricsOverview />
|
||||
|
||||
{/* Second Row - Charts in 6-6 columns */}
|
||||
<div className="grid gap-6 grid-cols-1 @5xl:grid-cols-2">
|
||||
<SalesChart />
|
||||
<RevenueBreakdown />
|
||||
</div>
|
||||
|
||||
{/* Third Row - Two Column Layout */}
|
||||
<div className="grid gap-6 grid-cols-1 @5xl:grid-cols-2">
|
||||
<RecentTransactions />
|
||||
<TopProducts />
|
||||
</div>
|
||||
|
||||
{/* Fourth Row - Customer Insights and Team Performance */}
|
||||
<CustomerInsights />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
type Point = { month: string; count: number };
|
||||
|
||||
export function CustomerGrowth({ data }: { data: Point[] }) {
|
||||
const total = data.reduce((s, p) => s + p.count, 0);
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Yeni müşteriler</CardTitle>
|
||||
<CardDescription>Son 6 ay — toplam {total} yeni müşteri</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[220px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={11}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={11}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ fill: "hsl(var(--muted))" }}
|
||||
contentStyle={{
|
||||
background: "hsl(var(--popover))",
|
||||
border: "1px solid hsl(var(--border))",
|
||||
borderRadius: 8,
|
||||
fontSize: 12,
|
||||
}}
|
||||
formatter={(value: unknown) => [`${value} müşteri`, "Yeni"]}
|
||||
/>
|
||||
<Bar dataKey="count" fill="hsl(var(--primary))" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { formatTRY } from "@/lib/format";
|
||||
|
||||
type Point = { month: string; income: number; expense: number };
|
||||
|
||||
export function IncomeChart({ data }: { data: Point[] }) {
|
||||
const total = data.reduce((s, p) => s + p.income, 0);
|
||||
|
||||
return (
|
||||
<Card className="@container">
|
||||
<CardHeader>
|
||||
<CardTitle>Gelir / Gider</CardTitle>
|
||||
<CardDescription>
|
||||
Son 12 ay — toplam gelir {formatTRY(total)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[280px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={data} margin={{ top: 8, right: 8, left: 8, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="incomeGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#10b981" stopOpacity={0.4} />
|
||||
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="expenseGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#ef4444" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="#ef4444" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={11}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={11}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
tickFormatter={(v) =>
|
||||
v >= 1000 ? `${(v / 1000).toFixed(0)}k` : String(v)
|
||||
}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: "hsl(var(--popover))",
|
||||
border: "1px solid hsl(var(--border))",
|
||||
borderRadius: 8,
|
||||
fontSize: 12,
|
||||
}}
|
||||
formatter={(value: number, name: string) => [
|
||||
formatTRY(value),
|
||||
name === "income" ? "Gelir" : "Gider",
|
||||
]}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="income"
|
||||
stroke="#10b981"
|
||||
strokeWidth={2}
|
||||
fill="url(#incomeGradient)"
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="expense"
|
||||
stroke="#ef4444"
|
||||
strokeWidth={2}
|
||||
fill="url(#expenseGradient)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
AlertCircle,
|
||||
ArrowDownRight,
|
||||
ArrowUpRight,
|
||||
CheckSquare,
|
||||
Receipt,
|
||||
Users,
|
||||
Wallet,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { formatTRY } from "@/lib/format";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import type { DashboardData } from "@/lib/appwrite/dashboard-queries";
|
||||
|
||||
function delta(current: number, previous: number): { pct: number; positive: boolean } | null {
|
||||
if (previous === 0) {
|
||||
if (current === 0) return null;
|
||||
return { pct: 100, positive: true };
|
||||
}
|
||||
const pct = ((current - previous) / previous) * 100;
|
||||
return { pct: Math.abs(pct), positive: pct >= 0 };
|
||||
}
|
||||
|
||||
export function Metrics({ data }: { data: DashboardData["metrics"] }) {
|
||||
const incomeDelta = delta(data.monthIncome, data.prevMonthIncome);
|
||||
|
||||
const cards = [
|
||||
{
|
||||
label: "Müşteriler",
|
||||
value: String(data.totalCustomers),
|
||||
sub: `${data.activeCustomers} aktif`,
|
||||
icon: Users,
|
||||
tone: "default",
|
||||
},
|
||||
{
|
||||
label: "Bu ayki gelir",
|
||||
value: formatTRY(data.monthIncome),
|
||||
sub: incomeDelta
|
||||
? `${incomeDelta.positive ? "+" : "−"}${incomeDelta.pct.toFixed(1)}% önceki ay`
|
||||
: "Geçen ay veri yok",
|
||||
icon: Wallet,
|
||||
tone: "income",
|
||||
trend: incomeDelta,
|
||||
},
|
||||
{
|
||||
label: "Bekleyen tahsilat",
|
||||
value: formatTRY(data.outstanding),
|
||||
sub:
|
||||
data.overdueCount > 0
|
||||
? `${data.overdueCount} vadesi geçmiş`
|
||||
: "Vadesi geçmiş yok",
|
||||
icon: Receipt,
|
||||
tone: data.overdueCount > 0 ? "warning" : "default",
|
||||
},
|
||||
{
|
||||
label: "Açık görevler",
|
||||
value: String(data.openTasks),
|
||||
sub:
|
||||
data.urgentTasks > 0 ? `${data.urgentTasks} acil` : "Acil görev yok",
|
||||
icon: CheckSquare,
|
||||
tone: data.urgentTasks > 0 ? "warning" : "default",
|
||||
},
|
||||
];
|
||||
|
||||
const toneClass: Record<string, string> = {
|
||||
default: "text-muted-foreground",
|
||||
income: "text-emerald-600 dark:text-emerald-400",
|
||||
warning: "text-amber-600 dark:text-amber-400",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 sm:grid-cols-2 @5xl:grid-cols-4">
|
||||
{cards.map((c) => {
|
||||
const Icon = c.icon;
|
||||
return (
|
||||
<Card key={c.label}>
|
||||
<CardContent className="flex items-start justify-between p-5">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs uppercase tracking-wide">
|
||||
{c.label}
|
||||
</p>
|
||||
<p className="mt-2 text-2xl font-semibold tabular-nums">{c.value}</p>
|
||||
<p
|
||||
className={cn(
|
||||
"mt-1 flex items-center gap-1 text-xs",
|
||||
c.tone === "warning" && data.overdueCount + data.urgentTasks > 0
|
||||
? "text-amber-600 dark:text-amber-400"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{c.trend &&
|
||||
(c.trend.positive ? (
|
||||
<ArrowUpRight className="text-emerald-600 dark:text-emerald-400 size-3" />
|
||||
) : (
|
||||
<ArrowDownRight className="text-red-600 dark:text-red-400 size-3" />
|
||||
))}
|
||||
{c.tone === "warning" && data.overdueCount + data.urgentTasks > 0 && (
|
||||
<AlertCircle className="size-3" />
|
||||
)}
|
||||
{c.sub}
|
||||
</p>
|
||||
</div>
|
||||
<Icon className={cn("size-5", toneClass[c.tone])} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import Link from "next/link";
|
||||
import { Calendar, FilePlus, Receipt, UserPlus } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function QuickActions() {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/customers">
|
||||
<UserPlus className="size-3.5" />
|
||||
Müşteri
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/invoices">
|
||||
<Receipt className="size-3.5" />
|
||||
Fatura
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/calendar">
|
||||
<Calendar className="size-3.5" />
|
||||
Etkinlik
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/tasks">
|
||||
<FilePlus className="size-3.5" />
|
||||
Görev
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, Receipt } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { formatDate, formatTRY } from "@/lib/format";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import type { DashboardData } from "@/lib/appwrite/dashboard-queries";
|
||||
|
||||
const TYPE_LABEL: Record<string, string> = {
|
||||
income: "Gelir",
|
||||
expense: "Gider",
|
||||
debt: "Borç",
|
||||
receivable: "Alacak",
|
||||
};
|
||||
|
||||
const TYPE_COLOR: Record<string, string> = {
|
||||
income: "text-emerald-600 dark:text-emerald-400",
|
||||
expense: "text-red-600 dark:text-red-400",
|
||||
debt: "text-amber-600 dark:text-amber-400",
|
||||
receivable: "text-blue-600 dark:text-blue-400",
|
||||
};
|
||||
|
||||
export function RecentTransactions({
|
||||
data,
|
||||
}: {
|
||||
data: DashboardData["recentTransactions"];
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Son işlemler</CardTitle>
|
||||
<CardDescription>En son finans hareketleri</CardDescription>
|
||||
</div>
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Link href="/finance">
|
||||
Tümü <ArrowRight className="size-3.5" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{data.length === 0 ? (
|
||||
<div className="text-muted-foreground flex flex-col items-center gap-2 py-10 text-sm">
|
||||
<Receipt className="size-6" />
|
||||
<p>Henüz finans hareketi yok.</p>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="divide-y">
|
||||
{data.map((t) => {
|
||||
const sign =
|
||||
t.type === "income" || t.type === "receivable" ? "+" : "−";
|
||||
return (
|
||||
<li key={t.id} className="flex items-center justify-between py-2.5">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{TYPE_LABEL[t.type]}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{formatDate(t.date)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-0.5 truncate text-sm">
|
||||
{t.customerName ? `${t.customerName} — ` : ""}
|
||||
{t.description || "—"}
|
||||
</p>
|
||||
</div>
|
||||
<span className={cn("font-medium tabular-nums", TYPE_COLOR[t.type])}>
|
||||
{sign} {formatTRY(t.amount)}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Crown, TrendingUp } from "lucide-react";
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { formatTRY } from "@/lib/format";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Item = { name: string; total: number };
|
||||
|
||||
export function TopCustomers({ data }: { data: Item[] }) {
|
||||
const max = data[0]?.total ?? 1;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Crown className="size-4" />
|
||||
En çok ciro yapan müşteriler
|
||||
</CardTitle>
|
||||
<CardDescription>Ödenmiş faturaların toplam tutarına göre</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{data.length === 0 ? (
|
||||
<div className="text-muted-foreground flex flex-col items-center gap-2 py-10 text-sm">
|
||||
<TrendingUp className="size-6" />
|
||||
<p>Henüz ödenmiş fatura yok.</p>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="space-y-3">
|
||||
{data.map((c, i) => {
|
||||
const width = (c.total / max) * 100;
|
||||
return (
|
||||
<li key={c.name + i} className="space-y-1.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="truncate text-sm font-medium">
|
||||
<span className="text-muted-foreground mr-2 tabular-nums">
|
||||
{String(i + 1).padStart(2, "0")}
|
||||
</span>
|
||||
{c.name}
|
||||
</span>
|
||||
<span className="text-sm tabular-nums">{formatTRY(c.total)}</span>
|
||||
</div>
|
||||
<div className="bg-muted h-1.5 overflow-hidden rounded-full">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full rounded-full",
|
||||
i === 0
|
||||
? "bg-emerald-500"
|
||||
: i === 1
|
||||
? "bg-emerald-400"
|
||||
: "bg-emerald-300",
|
||||
)}
|
||||
style={{ width: `${width}%` }}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getActiveContext } from "@/lib/appwrite/active-context";
|
||||
import { CustomerInsights } from "../dashboard-2/components/customer-insights";
|
||||
import { MetricsOverview } from "../dashboard-2/components/metrics-overview";
|
||||
import { QuickActions } from "../dashboard-2/components/quick-actions";
|
||||
import { RecentTransactions } from "../dashboard-2/components/recent-transactions";
|
||||
import { RevenueBreakdown } from "../dashboard-2/components/revenue-breakdown";
|
||||
import { SalesChart } from "../dashboard-2/components/sales-chart";
|
||||
import { TopProducts } from "../dashboard-2/components/top-products";
|
||||
import { getDashboardData } from "@/lib/appwrite/dashboard-queries";
|
||||
|
||||
import { CustomerGrowth } from "./components/customer-growth";
|
||||
import { IncomeChart } from "./components/income-chart";
|
||||
import { Metrics } from "./components/metrics";
|
||||
import { QuickActions } from "./components/quick-actions";
|
||||
import { RecentTransactions } from "./components/recent-transactions";
|
||||
import { TopCustomers } from "./components/top-customers";
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const ctx = await getActiveContext();
|
||||
if (!ctx) redirect("/onboarding");
|
||||
|
||||
const data = await getDashboardData(ctx.tenantId);
|
||||
|
||||
const firstName = ctx.user.name?.split(" ")[0] ?? "";
|
||||
const companyName = ctx.settings?.companyName ?? "Çalışma alanı";
|
||||
|
||||
@@ -32,19 +35,17 @@ export default async function DashboardPage() {
|
||||
</div>
|
||||
|
||||
<div className="@container/main space-y-6">
|
||||
<MetricsOverview />
|
||||
<Metrics data={data.metrics} />
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 @5xl:grid-cols-2">
|
||||
<SalesChart />
|
||||
<RevenueBreakdown />
|
||||
<IncomeChart data={data.monthlyIncome} />
|
||||
<TopCustomers data={data.topCustomers} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 @5xl:grid-cols-2">
|
||||
<RecentTransactions />
|
||||
<TopProducts />
|
||||
<RecentTransactions data={data.recentTransactions} />
|
||||
<CustomerGrowth data={data.newCustomersMonthly} />
|
||||
</div>
|
||||
|
||||
<CustomerInsights />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user