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 { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { getActiveContext } from "@/lib/appwrite/active-context";
|
import { getActiveContext } from "@/lib/appwrite/active-context";
|
||||||
import { CustomerInsights } from "../dashboard-2/components/customer-insights";
|
import { getDashboardData } from "@/lib/appwrite/dashboard-queries";
|
||||||
import { MetricsOverview } from "../dashboard-2/components/metrics-overview";
|
|
||||||
import { QuickActions } from "../dashboard-2/components/quick-actions";
|
import { CustomerGrowth } from "./components/customer-growth";
|
||||||
import { RecentTransactions } from "../dashboard-2/components/recent-transactions";
|
import { IncomeChart } from "./components/income-chart";
|
||||||
import { RevenueBreakdown } from "../dashboard-2/components/revenue-breakdown";
|
import { Metrics } from "./components/metrics";
|
||||||
import { SalesChart } from "../dashboard-2/components/sales-chart";
|
import { QuickActions } from "./components/quick-actions";
|
||||||
import { TopProducts } from "../dashboard-2/components/top-products";
|
import { RecentTransactions } from "./components/recent-transactions";
|
||||||
|
import { TopCustomers } from "./components/top-customers";
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
export default async function DashboardPage() {
|
||||||
const ctx = await getActiveContext();
|
const ctx = await getActiveContext();
|
||||||
if (!ctx) redirect("/onboarding");
|
if (!ctx) redirect("/onboarding");
|
||||||
|
|
||||||
|
const data = await getDashboardData(ctx.tenantId);
|
||||||
|
|
||||||
const firstName = ctx.user.name?.split(" ")[0] ?? "";
|
const firstName = ctx.user.name?.split(" ")[0] ?? "";
|
||||||
const companyName = ctx.settings?.companyName ?? "Çalışma alanı";
|
const companyName = ctx.settings?.companyName ?? "Çalışma alanı";
|
||||||
|
|
||||||
@@ -32,19 +35,17 @@ export default async function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="@container/main space-y-6">
|
<div className="@container/main space-y-6">
|
||||||
<MetricsOverview />
|
<Metrics data={data.metrics} />
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 @5xl:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 @5xl:grid-cols-2">
|
||||||
<SalesChart />
|
<IncomeChart data={data.monthlyIncome} />
|
||||||
<RevenueBreakdown />
|
<TopCustomers data={data.topCustomers} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 @5xl:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 @5xl:grid-cols-2">
|
||||||
<RecentTransactions />
|
<RecentTransactions data={data.recentTransactions} />
|
||||||
<TopProducts />
|
<CustomerGrowth data={data.newCustomersMonthly} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CustomerInsights />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useRouter } from "next/navigation"
|
|||||||
import { Command as CommandPrimitive } from "cmdk"
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
LayoutPanelLeft,
|
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Mail,
|
Mail,
|
||||||
CheckSquare,
|
CheckSquare,
|
||||||
@@ -17,6 +16,7 @@ import {
|
|||||||
HelpCircle,
|
HelpCircle,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
User,
|
User,
|
||||||
|
Users,
|
||||||
Bell,
|
Bell,
|
||||||
Link2,
|
Link2,
|
||||||
Palette,
|
Palette,
|
||||||
@@ -127,11 +127,23 @@ export function CommandSearch({ open, onOpenChange }: CommandSearchProps) {
|
|||||||
const commandRef = React.useRef<HTMLDivElement>(null)
|
const commandRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const searchItems: SearchItem[] = [
|
const searchItems: SearchItem[] = [
|
||||||
// Dashboards
|
// Genel
|
||||||
{ title: "Dashboard 1", url: "/dashboard", group: "Dashboards", icon: LayoutDashboard },
|
{ title: "Genel bakış", url: "/dashboard", group: "Genel", icon: LayoutDashboard },
|
||||||
{ title: "Dashboard 2", url: "/dashboard-2", group: "Dashboards", icon: LayoutPanelLeft },
|
|
||||||
|
|
||||||
// Apps
|
// İşletme
|
||||||
|
{ title: "Müşteriler", url: "/customers", group: "İşletme", icon: Users },
|
||||||
|
{ title: "Hizmetler", url: "/services", group: "İşletme", icon: Mail },
|
||||||
|
{ title: "Yazılımlarımız", url: "/software", group: "İşletme", icon: Mail },
|
||||||
|
|
||||||
|
// Operasyon
|
||||||
|
{ title: "Takvim", url: "/calendar", group: "Operasyon", icon: Calendar },
|
||||||
|
{ title: "Görevler", url: "/tasks", group: "Operasyon", icon: CheckSquare },
|
||||||
|
|
||||||
|
// Finans
|
||||||
|
{ title: "Gelir / Gider", url: "/finance", group: "Finans", icon: Mail },
|
||||||
|
{ title: "Faturalar", url: "/invoices", group: "Finans", icon: Mail },
|
||||||
|
|
||||||
|
// Apps (legacy template)
|
||||||
{ title: "Mail", url: "/mail", group: "Apps", icon: Mail },
|
{ title: "Mail", url: "/mail", group: "Apps", icon: Mail },
|
||||||
{ title: "Tasks", url: "/tasks", group: "Apps", icon: CheckSquare },
|
{ title: "Tasks", url: "/tasks", group: "Apps", icon: CheckSquare },
|
||||||
{ title: "Chat", url: "/chat", group: "Apps", icon: MessageCircle },
|
{ title: "Chat", url: "/chat", group: "Apps", icon: MessageCircle },
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import "server-only";
|
||||||
|
|
||||||
|
import { Query } from "node-appwrite";
|
||||||
|
|
||||||
|
import { createAdminClient } from "./server";
|
||||||
|
import {
|
||||||
|
DATABASE_ID,
|
||||||
|
TABLES,
|
||||||
|
type Customer,
|
||||||
|
type FinanceEntry,
|
||||||
|
type Invoice,
|
||||||
|
type Task,
|
||||||
|
} from "./schema";
|
||||||
|
|
||||||
|
export type DashboardData = {
|
||||||
|
metrics: {
|
||||||
|
totalCustomers: number;
|
||||||
|
activeCustomers: number;
|
||||||
|
monthIncome: number; // current month income
|
||||||
|
prevMonthIncome: number; // previous month income (for delta)
|
||||||
|
outstanding: number; // unpaid invoices total (draft+sent+overdue)
|
||||||
|
overdueCount: number;
|
||||||
|
openTasks: number;
|
||||||
|
urgentTasks: number;
|
||||||
|
};
|
||||||
|
monthlyIncome: { month: string; income: number; expense: number }[]; // last 12 months
|
||||||
|
topCustomers: { name: string; total: number }[]; // top 5 by paid invoice total
|
||||||
|
recentTransactions: {
|
||||||
|
id: string;
|
||||||
|
type: FinanceEntry["type"];
|
||||||
|
amount: number;
|
||||||
|
date: string;
|
||||||
|
customerName: string;
|
||||||
|
description: string;
|
||||||
|
}[];
|
||||||
|
topServices: { name: string; total: number; count: number }[]; // top 5 services by revenue (qty*unitPrice)
|
||||||
|
newCustomersMonthly: { month: string; count: number }[]; // last 6 months
|
||||||
|
};
|
||||||
|
|
||||||
|
const MONTH_SHORT = ["Oca", "Şub", "Mar", "Nis", "May", "Haz", "Tem", "Ağu", "Eyl", "Eki", "Kas", "Ara"];
|
||||||
|
|
||||||
|
function monthKey(d: Date): string {
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function monthLabel(d: Date): string {
|
||||||
|
return MONTH_SHORT[d.getMonth()];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDashboardData(tenantId: string): Promise<DashboardData> {
|
||||||
|
const { tablesDB } = createAdminClient();
|
||||||
|
|
||||||
|
const [customers, invoices, financeEntries, tasks, services] = await Promise.all([
|
||||||
|
tablesDB
|
||||||
|
.listRows({
|
||||||
|
databaseId: DATABASE_ID,
|
||||||
|
tableId: TABLES.customers,
|
||||||
|
queries: [Query.equal("tenantId", tenantId), Query.limit(2000)],
|
||||||
|
})
|
||||||
|
.catch(() => ({ rows: [] as unknown[] })),
|
||||||
|
tablesDB
|
||||||
|
.listRows({
|
||||||
|
databaseId: DATABASE_ID,
|
||||||
|
tableId: TABLES.invoices,
|
||||||
|
queries: [Query.equal("tenantId", tenantId), Query.limit(2000)],
|
||||||
|
})
|
||||||
|
.catch(() => ({ rows: [] as unknown[] })),
|
||||||
|
tablesDB
|
||||||
|
.listRows({
|
||||||
|
databaseId: DATABASE_ID,
|
||||||
|
tableId: TABLES.financeEntries,
|
||||||
|
queries: [
|
||||||
|
Query.equal("tenantId", tenantId),
|
||||||
|
Query.orderDesc("date"),
|
||||||
|
Query.limit(2000),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.catch(() => ({ rows: [] as unknown[] })),
|
||||||
|
tablesDB
|
||||||
|
.listRows({
|
||||||
|
databaseId: DATABASE_ID,
|
||||||
|
tableId: TABLES.tasks,
|
||||||
|
queries: [Query.equal("tenantId", tenantId), Query.limit(2000)],
|
||||||
|
})
|
||||||
|
.catch(() => ({ rows: [] as unknown[] })),
|
||||||
|
tablesDB
|
||||||
|
.listRows({
|
||||||
|
databaseId: DATABASE_ID,
|
||||||
|
tableId: TABLES.services,
|
||||||
|
queries: [Query.equal("tenantId", tenantId), Query.limit(2000)],
|
||||||
|
})
|
||||||
|
.catch(() => ({ rows: [] as unknown[] })),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const customerList = customers.rows as unknown as Customer[];
|
||||||
|
const invoiceList = invoices.rows as unknown as Invoice[];
|
||||||
|
const entryList = financeEntries.rows as unknown as FinanceEntry[];
|
||||||
|
const taskList = tasks.rows as unknown as Task[];
|
||||||
|
|
||||||
|
const customerMap = new Map(customerList.map((c) => [c.$id, c.name]));
|
||||||
|
|
||||||
|
// ---------------- Metrics ----------------
|
||||||
|
const now = new Date();
|
||||||
|
const thisMonth = monthKey(now);
|
||||||
|
const prev = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||||
|
const prevMonth = monthKey(prev);
|
||||||
|
|
||||||
|
let monthIncome = 0;
|
||||||
|
let prevMonthIncome = 0;
|
||||||
|
for (const e of entryList) {
|
||||||
|
if (e.type !== "income") continue;
|
||||||
|
const k = monthKey(new Date(e.date));
|
||||||
|
if (k === thisMonth) monthIncome += e.amount;
|
||||||
|
else if (k === prevMonth) prevMonthIncome += e.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
let outstanding = 0;
|
||||||
|
let overdueCount = 0;
|
||||||
|
const today = new Date();
|
||||||
|
for (const inv of invoiceList) {
|
||||||
|
const status = inv.status ?? "draft";
|
||||||
|
if (status === "paid" || status === "cancelled") continue;
|
||||||
|
outstanding += inv.total ?? 0;
|
||||||
|
if (status === "overdue" || (inv.dueDate && new Date(inv.dueDate) < today)) {
|
||||||
|
overdueCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let openTasks = 0;
|
||||||
|
let urgentTasks = 0;
|
||||||
|
for (const t of taskList) {
|
||||||
|
if ((t.status ?? "todo") !== "done") {
|
||||||
|
openTasks += 1;
|
||||||
|
if ((t.priority ?? "medium") === "urgent") urgentTasks += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeCustomers = customerList.filter((c) => (c.status ?? "active") === "active").length;
|
||||||
|
|
||||||
|
// ---------------- Monthly income/expense (last 12 months) ----------------
|
||||||
|
const monthSeries: DashboardData["monthlyIncome"] = [];
|
||||||
|
for (let i = 11; i >= 0; i--) {
|
||||||
|
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||||
|
monthSeries.push({ month: monthLabel(d), income: 0, expense: 0 });
|
||||||
|
}
|
||||||
|
for (const e of entryList) {
|
||||||
|
const ed = new Date(e.date);
|
||||||
|
const monthsAgo =
|
||||||
|
(now.getFullYear() - ed.getFullYear()) * 12 + (now.getMonth() - ed.getMonth());
|
||||||
|
if (monthsAgo < 0 || monthsAgo > 11) continue;
|
||||||
|
const idx = 11 - monthsAgo;
|
||||||
|
if (e.type === "income") monthSeries[idx].income += e.amount;
|
||||||
|
else if (e.type === "expense") monthSeries[idx].expense += e.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Top customers (by paid invoice total) ----------------
|
||||||
|
const customerRevenue = new Map<string, number>();
|
||||||
|
for (const inv of invoiceList) {
|
||||||
|
if (inv.status !== "paid") continue;
|
||||||
|
customerRevenue.set(
|
||||||
|
inv.customerId,
|
||||||
|
(customerRevenue.get(inv.customerId) ?? 0) + (inv.total ?? 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const topCustomers = Array.from(customerRevenue.entries())
|
||||||
|
.map(([id, total]) => ({ name: customerMap.get(id) ?? "—", total }))
|
||||||
|
.sort((a, b) => b.total - a.total)
|
||||||
|
.slice(0, 5);
|
||||||
|
|
||||||
|
// ---------------- Recent transactions (last 8) ----------------
|
||||||
|
const recentTransactions = entryList.slice(0, 8).map((e) => ({
|
||||||
|
id: e.$id,
|
||||||
|
type: e.type,
|
||||||
|
amount: e.amount,
|
||||||
|
date: e.date,
|
||||||
|
customerName: e.customerId ? customerMap.get(e.customerId) ?? "" : "",
|
||||||
|
description: e.description ?? "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ---------------- Top services (by current MRR estimate) ----------------
|
||||||
|
const svcMap = new Map<string, { name: string; total: number; count: number }>();
|
||||||
|
for (const s of services.rows as unknown as { name: string; unitPrice?: number }[]) {
|
||||||
|
const key = s.name;
|
||||||
|
const entry = svcMap.get(key) ?? { name: s.name, total: 0, count: 0 };
|
||||||
|
entry.total += s.unitPrice ?? 0;
|
||||||
|
entry.count += 1;
|
||||||
|
svcMap.set(key, entry);
|
||||||
|
}
|
||||||
|
const topServices = Array.from(svcMap.values())
|
||||||
|
.sort((a, b) => b.total - a.total)
|
||||||
|
.slice(0, 5);
|
||||||
|
|
||||||
|
// ---------------- New customers per month (last 6) ----------------
|
||||||
|
const newCustomersMonthly: DashboardData["newCustomersMonthly"] = [];
|
||||||
|
for (let i = 5; i >= 0; i--) {
|
||||||
|
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||||
|
newCustomersMonthly.push({ month: monthLabel(d), count: 0 });
|
||||||
|
}
|
||||||
|
for (const c of customerList) {
|
||||||
|
const cd = new Date(c.$createdAt);
|
||||||
|
const monthsAgo =
|
||||||
|
(now.getFullYear() - cd.getFullYear()) * 12 + (now.getMonth() - cd.getMonth());
|
||||||
|
if (monthsAgo < 0 || monthsAgo > 5) continue;
|
||||||
|
const idx = 5 - monthsAgo;
|
||||||
|
newCustomersMonthly[idx].count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
metrics: {
|
||||||
|
totalCustomers: customerList.length,
|
||||||
|
activeCustomers,
|
||||||
|
monthIncome,
|
||||||
|
prevMonthIncome,
|
||||||
|
outstanding,
|
||||||
|
overdueCount,
|
||||||
|
openTasks,
|
||||||
|
urgentTasks,
|
||||||
|
},
|
||||||
|
monthlyIncome: monthSeries,
|
||||||
|
topCustomers,
|
||||||
|
recentTransactions,
|
||||||
|
topServices,
|
||||||
|
newCustomersMonthly,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user