feat: implement analytics dashboard page with custom Recharts visualizations and Supabase RPC integration
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
||||
import { Card, CardContent } from "poyraz-ui/atoms";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "poyraz-ui/molecules";
|
||||
import {
|
||||
@@ -10,9 +10,12 @@ import {
|
||||
import { BarChart3, Filter } from "lucide-react";
|
||||
|
||||
export type AnalyticsData = {
|
||||
tasks: { id: string; status: string; created_at: string; due_at?: string }[];
|
||||
projects: { id: string; name: string }[];
|
||||
finances: { id: string; type: string; amount: number; transaction_date: string; project_id?: string }[];
|
||||
metrics: {
|
||||
projectIncomeData: { name: string; value: number }[];
|
||||
completedTasks: number;
|
||||
activeTasks: number;
|
||||
};
|
||||
range: string;
|
||||
};
|
||||
|
||||
type AnalyticsClientProps = {
|
||||
@@ -22,44 +25,17 @@ type AnalyticsClientProps = {
|
||||
const COLORS = ["hsl(var(--primary))", "hsl(var(--destructive))", "#eab308", "#3b82f6", "#8b5cf6"];
|
||||
|
||||
export function AnalyticsClient({ data }: AnalyticsClientProps) {
|
||||
const [dateRange, setDateRange] = useState("this_month");
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const filterByDate = (dateStr: string | null) => {
|
||||
if (!dateStr) return false;
|
||||
const date = new Date(dateStr);
|
||||
if (dateRange === "this_week") {
|
||||
const firstDay = new Date(now.setDate(now.getDate() - now.getDay() + (now.getDay() === 0 ? -6 : 1)));
|
||||
return date >= firstDay;
|
||||
}
|
||||
if (dateRange === "this_month") {
|
||||
return date.getMonth() === now.getMonth() && date.getFullYear() === now.getFullYear();
|
||||
}
|
||||
if (dateRange === "this_year") {
|
||||
return date.getFullYear() === now.getFullYear();
|
||||
}
|
||||
return true;
|
||||
const handleRangeChange = (newRange: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set("range", newRange);
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
};
|
||||
|
||||
const filteredFinances = data.finances.filter(f => filterByDate(f.transaction_date));
|
||||
const filteredTasks = data.tasks.filter(t => filterByDate(t.created_at || t.due_at || null));
|
||||
|
||||
// Project-based income
|
||||
const projectIncomeMap = new Map();
|
||||
filteredFinances.filter(f => f.type === "income" && f.project_id).forEach(f => {
|
||||
const project = data.projects.find(p => p.id === f.project_id);
|
||||
const name = project ? project.name : "Bilinmeyen";
|
||||
if (!projectIncomeMap.has(name)) {
|
||||
projectIncomeMap.set(name, { name, value: 0 });
|
||||
}
|
||||
projectIncomeMap.get(name).value += Number(f.amount);
|
||||
});
|
||||
const projectIncomeData = Array.from(projectIncomeMap.values());
|
||||
|
||||
// Task completion stats
|
||||
const completedTasks = filteredTasks.filter(t => t.status === "done").length;
|
||||
const activeTasks = filteredTasks.filter(t => t.status !== "done" && t.status !== "cancelled").length;
|
||||
const { projectIncomeData, completedTasks, activeTasks } = data.metrics;
|
||||
|
||||
const taskStatusData = [
|
||||
{ name: "Tamamlanan", value: completedTasks },
|
||||
@@ -85,7 +61,7 @@ export function AnalyticsClient({ data }: AnalyticsClientProps) {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Select value={dateRange} onValueChange={setDateRange}>
|
||||
<Select value={data.range} onValueChange={handleRangeChange}>
|
||||
<SelectTrigger className="w-[160px]">
|
||||
<SelectValue placeholder="Tarih aralığı" />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -6,7 +6,11 @@ export const metadata = {
|
||||
title: "Analizler - Neta",
|
||||
};
|
||||
|
||||
export default async function AnalyticsPage() {
|
||||
export default async function AnalyticsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
}) {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
@@ -14,24 +18,41 @@ export default async function AnalyticsPage() {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
// Fetch all analytics data in parallel with only needed columns
|
||||
const [{ data: tasks }, { data: projects }, { data: finances }] =
|
||||
await Promise.all([
|
||||
supabase
|
||||
.from("tasks")
|
||||
.select("id, status, created_at, due_at"),
|
||||
supabase
|
||||
.from("projects")
|
||||
.select("id, name"),
|
||||
supabase
|
||||
.from("finance_transactions")
|
||||
.select("id, type, amount, transaction_date, project_id"),
|
||||
]);
|
||||
const range = typeof searchParams.range === "string" ? searchParams.range : "this_month";
|
||||
|
||||
const now = new Date();
|
||||
let startDate = new Date();
|
||||
let endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
|
||||
|
||||
if (range === "this_week") {
|
||||
const tempNow = new Date();
|
||||
const firstDay = new Date(tempNow.setDate(tempNow.getDate() - tempNow.getDay() + (tempNow.getDay() === 0 ? -6 : 1)));
|
||||
firstDay.setHours(0, 0, 0, 0);
|
||||
startDate = firstDay;
|
||||
endDate = new Date(firstDay.getTime());
|
||||
endDate.setDate(endDate.getDate() + 6);
|
||||
endDate.setHours(23, 59, 59, 999);
|
||||
} else if (range === "this_month") {
|
||||
startDate = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0);
|
||||
endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
|
||||
} else if (range === "this_year") {
|
||||
startDate = new Date(now.getFullYear(), 0, 1, 0, 0, 0);
|
||||
endDate = new Date(now.getFullYear(), 11, 31, 23, 59, 59);
|
||||
}
|
||||
|
||||
// Fetch metrics using RPC
|
||||
const { data: metricsData } = await supabase.rpc('get_analytics_metrics', {
|
||||
p_start_date: startDate.toISOString(),
|
||||
p_end_date: endDate.toISOString()
|
||||
});
|
||||
|
||||
const analyticsData = {
|
||||
tasks: tasks || [],
|
||||
projects: projects || [],
|
||||
finances: finances || [],
|
||||
metrics: metricsData || {
|
||||
projectIncomeData: [],
|
||||
completedTasks: 0,
|
||||
activeTasks: 0
|
||||
},
|
||||
range
|
||||
};
|
||||
|
||||
return <AnalyticsClient data={analyticsData} />;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
||||
import { PendingLink } from "@/components/ui/pending-link";
|
||||
import { Badge, Card, CardContent } from "poyraz-ui/atoms";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "poyraz-ui/molecules";
|
||||
@@ -8,11 +8,17 @@ import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxi
|
||||
import { CheckCircle2, Wallet, FolderKanban, Activity, Users } from "lucide-react";
|
||||
|
||||
export type DashboardData = {
|
||||
tasks: { id: string; status: string; created_at: string; updated_at?: string; due_at?: string }[];
|
||||
metrics: {
|
||||
netProfit: number;
|
||||
activeProjectsCount: number;
|
||||
completedTasksCount: number;
|
||||
avgMood: string;
|
||||
financeTrend: { date: string; income: number; expense: number }[];
|
||||
moodTrend: { date: string; mood: number; energy: number }[];
|
||||
};
|
||||
projects: { id: string; status: string; name: string; created_at: string }[];
|
||||
finances: { id: string; type: string; amount: number; transaction_date: string }[];
|
||||
logs: { id: string; log_date: string; mood_score: number; energy_score: number }[];
|
||||
clients: { id: string; name: string; company_name: string; created_at: string }[];
|
||||
range: string;
|
||||
};
|
||||
|
||||
type DashboardClientProps = {
|
||||
@@ -20,62 +26,29 @@ type DashboardClientProps = {
|
||||
};
|
||||
|
||||
export function DashboardClient({ data }: DashboardClientProps) {
|
||||
const [dateRange, setDateRange] = useState("this_month");
|
||||
|
||||
// Calculate real metrics from the `data` prop depending on `dateRange`
|
||||
const now = new Date();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// Helper to filter by date
|
||||
const filterByDate = (dateStr: string | null) => {
|
||||
if (!dateStr) return false;
|
||||
const date = new Date(dateStr);
|
||||
if (dateRange === "today") {
|
||||
return date.toDateString() === now.toDateString();
|
||||
}
|
||||
if (dateRange === "this_week") {
|
||||
const firstDay = new Date(now.setDate(now.getDate() - now.getDay() + (now.getDay() === 0 ? -6 : 1))); // Monday
|
||||
return date >= firstDay;
|
||||
}
|
||||
if (dateRange === "this_month") {
|
||||
return date.getMonth() === now.getMonth() && date.getFullYear() === now.getFullYear();
|
||||
}
|
||||
return true;
|
||||
const handleRangeChange = (newRange: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set("range", newRange);
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
};
|
||||
|
||||
// KPIs
|
||||
const filteredFinances = (data.finances || []).filter(f => filterByDate(f.transaction_date));
|
||||
const income = filteredFinances.filter(f => f.type === "income").reduce((acc, curr) => acc + Number(curr.amount), 0);
|
||||
const expense = filteredFinances.filter(f => f.type === "expense").reduce((acc, curr) => acc + Number(curr.amount), 0);
|
||||
const netProfit = income - expense;
|
||||
const { netProfit, activeProjectsCount, completedTasksCount, avgMood, financeTrend, moodTrend } = data.metrics;
|
||||
|
||||
const activeProjectsCount = (data.projects || []).filter(p => p.status === "active").length;
|
||||
|
||||
const completedTasksCount = (data.tasks || []).filter(t => t.status === "done" && filterByDate(t.updated_at || t.created_at)).length;
|
||||
// Format dates for Recharts using local timezone
|
||||
const incomeTrendData = (financeTrend || []).map(f => ({
|
||||
name: new Date(f.date).toLocaleDateString("tr-TR", { month: "short", day: "numeric" }),
|
||||
income: f.income,
|
||||
expense: f.expense
|
||||
}));
|
||||
|
||||
const filteredLogs = (data.logs || []).filter(l => filterByDate(l.log_date));
|
||||
const avgMood = filteredLogs.length > 0
|
||||
? (filteredLogs.reduce((acc, curr) => acc + curr.mood_score, 0) / filteredLogs.length).toFixed(1)
|
||||
: "0.0";
|
||||
|
||||
// Recharts Data Prep
|
||||
// Group finances by date
|
||||
const financeMap = new Map();
|
||||
filteredFinances.forEach(f => {
|
||||
const d = new Date(f.transaction_date).toLocaleDateString("tr-TR", { month: "short", day: "numeric" });
|
||||
if (!financeMap.has(d)) {
|
||||
financeMap.set(d, { name: d, income: 0, expense: 0 });
|
||||
}
|
||||
const entry = financeMap.get(d);
|
||||
if (f.type === "income") entry.income += Number(f.amount);
|
||||
if (f.type === "expense") entry.expense += Number(f.amount);
|
||||
});
|
||||
const incomeTrendData = Array.from(financeMap.values());
|
||||
|
||||
// Group logs by date
|
||||
const moodTrendData = filteredLogs.map(l => ({
|
||||
date: new Date(l.log_date).toLocaleDateString("tr-TR", { month: "short", day: "numeric" }),
|
||||
mood: l.mood_score,
|
||||
energy: l.energy_score,
|
||||
const moodTrendData = (moodTrend || []).map(l => ({
|
||||
date: new Date(l.date).toLocaleDateString("tr-TR", { month: "short", day: "numeric" }),
|
||||
mood: l.mood,
|
||||
energy: l.energy,
|
||||
}));
|
||||
|
||||
// Format currency
|
||||
@@ -107,7 +80,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Select value={dateRange} onValueChange={setDateRange}>
|
||||
<Select value={data.range} onValueChange={handleRangeChange}>
|
||||
<SelectTrigger className="w-[160px]">
|
||||
<SelectValue placeholder="Tarih aralığı" />
|
||||
</SelectTrigger>
|
||||
|
||||
+47
-30
@@ -6,7 +6,11 @@ export const metadata = {
|
||||
title: "Dashboard - Neta",
|
||||
};
|
||||
|
||||
export default async function DashboardPage() {
|
||||
export default async function DashboardPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
}) {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
@@ -14,41 +18,48 @@ export default async function DashboardPage() {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
// Get current date and date 30 days ago
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
const range = typeof searchParams.range === "string" ? searchParams.range : "this_month";
|
||||
|
||||
const todayStr = today.toISOString();
|
||||
const thirtyDaysAgoStr = thirtyDaysAgo.toISOString();
|
||||
const now = new Date();
|
||||
let startDate = new Date();
|
||||
let endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59); // default to end of month
|
||||
|
||||
if (range === "today") {
|
||||
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
|
||||
endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
|
||||
} else if (range === "this_week") {
|
||||
// Reset `now` because setDate mutates
|
||||
const tempNow = new Date();
|
||||
const firstDay = new Date(tempNow.setDate(tempNow.getDate() - tempNow.getDay() + (tempNow.getDay() === 0 ? -6 : 1)));
|
||||
firstDay.setHours(0, 0, 0, 0);
|
||||
startDate = firstDay;
|
||||
endDate = new Date(firstDay.getTime());
|
||||
endDate.setDate(endDate.getDate() + 6);
|
||||
endDate.setHours(23, 59, 59, 999);
|
||||
} else if (range === "this_month") {
|
||||
startDate = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0);
|
||||
endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
|
||||
} else if (range === "this_year") {
|
||||
startDate = new Date(now.getFullYear(), 0, 1, 0, 0, 0);
|
||||
endDate = new Date(now.getFullYear(), 11, 31, 23, 59, 59);
|
||||
}
|
||||
|
||||
// Fetch all dashboard data in parallel with only needed columns
|
||||
// Fetch metrics using RPC
|
||||
const { data: metricsData } = await supabase.rpc('get_dashboard_metrics', {
|
||||
p_start_date: startDate.toISOString(),
|
||||
p_end_date: endDate.toISOString()
|
||||
});
|
||||
|
||||
// Fetch limited recent data
|
||||
const [
|
||||
{ data: tasks },
|
||||
{ data: projects },
|
||||
{ data: finances },
|
||||
{ data: logs },
|
||||
{ data: clients },
|
||||
] = await Promise.all([
|
||||
supabase
|
||||
.from("tasks")
|
||||
.select("id, status, created_at, updated_at, due_at")
|
||||
.or(`due_at.gte.${thirtyDaysAgoStr},created_at.gte.${thirtyDaysAgoStr}`)
|
||||
.order("due_at", { ascending: true }),
|
||||
supabase
|
||||
.from("projects")
|
||||
.select("id, status, name, created_at")
|
||||
.order("created_at", { ascending: false }),
|
||||
supabase
|
||||
.from("finance_transactions")
|
||||
.select("id, type, amount, transaction_date")
|
||||
.gte("transaction_date", thirtyDaysAgoStr)
|
||||
.order("transaction_date", { ascending: true }),
|
||||
supabase
|
||||
.from("daily_logs")
|
||||
.select("id, log_date, mood_score, energy_score")
|
||||
.gte("log_date", thirtyDaysAgoStr)
|
||||
.order("log_date", { ascending: true }),
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(5),
|
||||
supabase
|
||||
.from("clients")
|
||||
.select("id, name, company_name, created_at")
|
||||
@@ -57,11 +68,17 @@ export default async function DashboardPage() {
|
||||
]);
|
||||
|
||||
const dashboardData = {
|
||||
tasks: tasks || [],
|
||||
metrics: metricsData || {
|
||||
netProfit: 0,
|
||||
activeProjectsCount: 0,
|
||||
completedTasksCount: 0,
|
||||
avgMood: "0.0",
|
||||
financeTrend: [],
|
||||
moodTrend: []
|
||||
},
|
||||
projects: projects || [],
|
||||
finances: finances || [],
|
||||
logs: logs || [],
|
||||
clients: clients || [],
|
||||
range
|
||||
};
|
||||
|
||||
return <DashboardClient data={dashboardData} />;
|
||||
|
||||
Reference in New Issue
Block a user