Files
isletmem-kovakcrm/src/app/(dashboard)/dashboard/components/metrics.tsx
T
kovakmedya f11cd099f6 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).
2026-04-30 06:19:44 +03:00

113 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}