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:
kovakmedya
2026-04-30 06:19:44 +03:00
parent 37777a71f9
commit f11cd099f6
19 changed files with 698 additions and 1123 deletions
@@ -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"
}
-47
View File
@@ -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>
);
}
+15 -14
View File
@@ -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>
);