refactor: decouple analytics page into server-side data fetching and client-side presentation components
This commit is contained in:
@@ -0,0 +1,166 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Card, CardContent } from "poyraz-ui/atoms";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "poyraz-ui/molecules";
|
||||||
|
import {
|
||||||
|
Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis,
|
||||||
|
PieChart, Pie, Cell, Legend
|
||||||
|
} from "recharts";
|
||||||
|
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 }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type AnalyticsClientProps = {
|
||||||
|
data: AnalyticsData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLORS = ["hsl(var(--primary))", "hsl(var(--destructive))", "#eab308", "#3b82f6", "#8b5cf6"];
|
||||||
|
|
||||||
|
export function AnalyticsClient({ data }: AnalyticsClientProps) {
|
||||||
|
const [dateRange, setDateRange] = useState("this_month");
|
||||||
|
|
||||||
|
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 filteredFinances = data.finances.filter(f => filterByDate(f.transaction_date));
|
||||||
|
const filteredTasks = data.tasks.filter(t => filterByDate(t.created_at || t.due_at));
|
||||||
|
|
||||||
|
// 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 === "completed").length;
|
||||||
|
const activeTasks = filteredTasks.filter(t => t.status !== "completed" && t.status !== "cancelled").length;
|
||||||
|
|
||||||
|
const taskStatusData = [
|
||||||
|
{ name: "Tamamlanan", value: completedTasks },
|
||||||
|
{ name: "Devam Eden", value: activeTasks }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex max-w-7xl flex-col gap-6">
|
||||||
|
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<BarChart3 className="h-4 w-4" />
|
||||||
|
Analizler
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-semibold tracking-normal text-foreground">
|
||||||
|
Performans ve Finans Analizi
|
||||||
|
</h1>
|
||||||
|
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">
|
||||||
|
Müşteri bazlı gelirler, görev tamamlama oranları ve proje ilerleme grafikleri.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Select value={dateRange} onValueChange={setDateRange}>
|
||||||
|
<SelectTrigger className="w-[160px]">
|
||||||
|
<SelectValue placeholder="Tarih aralığı" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="this_week">Bu Hafta</SelectItem>
|
||||||
|
<SelectItem value="this_month">Bu Ay</SelectItem>
|
||||||
|
<SelectItem value="this_year">Bu Yıl</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<h3 className="mb-6 text-sm font-semibold text-foreground">Proje Bazlı Gelir Dağılımı</h3>
|
||||||
|
<div className="h-[300px] w-full">
|
||||||
|
{projectIncomeData.length > 0 ? (
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={projectIncomeData}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={60}
|
||||||
|
outerRadius={80}
|
||||||
|
paddingAngle={5}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{projectIncomeData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip
|
||||||
|
formatter={(value: number) => `₺${value}`}
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: 'hsl(var(--background))',
|
||||||
|
borderColor: 'hsl(var(--border))',
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Legend />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">Veri bulunamadı.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<h3 className="mb-6 text-sm font-semibold text-foreground">Görev Durumu Analizi</h3>
|
||||||
|
<div className="h-[300px] w-full">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart data={taskStatusData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
|
||||||
|
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }} dy={10} />
|
||||||
|
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }} dx={-10} />
|
||||||
|
<Tooltip
|
||||||
|
cursor={{ fill: 'hsl(var(--muted))', opacity: 0.4 }}
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: 'hsl(var(--background))',
|
||||||
|
borderColor: 'hsl(var(--border))',
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="value" fill="hsl(var(--primary))" radius={[4, 4, 0, 0]} barSize={60} />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,255 +1,39 @@
|
|||||||
"use client";
|
import { createClient } from "@/lib/supabase/server";
|
||||||
|
import { AnalyticsClient } from "./analytics-client";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { useState } from "react";
|
export const metadata = {
|
||||||
import {
|
title: "Analizler - Cognis",
|
||||||
ArrowUpRight, Brain, TrendingUp, Zap, Target, ArrowDownRight,
|
};
|
||||||
BarChart3, Filter, Download, Info, CheckCircle2, AlertCircle
|
|
||||||
} from "lucide-react";
|
|
||||||
import {
|
|
||||||
Area, AreaChart, Bar, BarChart, CartesianGrid, ResponsiveContainer,
|
|
||||||
Tooltip, XAxis, YAxis, Radar, RadarChart, PolarGrid, PolarAngleAxis, Cell
|
|
||||||
} from "recharts";
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
|
|
||||||
// Mock Data
|
export default async function AnalyticsPage() {
|
||||||
const productivityTrend = [
|
const supabase = await createClient();
|
||||||
{ date: "May 1", focus: 65, energy: 40 },
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
{ date: "May 4", focus: 75, energy: 55 },
|
|
||||||
{ date: "May 8", focus: 60, energy: 50 },
|
|
||||||
{ date: "May 12", focus: 85, energy: 70 },
|
|
||||||
{ date: "May 16", focus: 72, energy: 65 },
|
|
||||||
{ date: "May 20", focus: 95, energy: 85 },
|
|
||||||
{ date: "May 24", focus: 88, energy: 80 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const habitCompletion = [
|
if (!user) {
|
||||||
{ name: "Reading", completed: 85, missed: 15, color: "#6C5BB0" },
|
redirect("/login");
|
||||||
{ name: "Workout", completed: 60, missed: 40, color: "#a798e8" },
|
}
|
||||||
{ name: "Meditation", completed: 90, missed: 10, color: "#10b981" },
|
|
||||||
{ name: "Coding", completed: 75, missed: 25, color: "#3b82f6" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const focusRadarData = [
|
// 1. Fetch tasks
|
||||||
{ subject: "Deep Work", A: 120, fullMark: 150 },
|
const { data: tasks } = await supabase
|
||||||
{ subject: "Learning", A: 98, fullMark: 150 },
|
.from("tasks")
|
||||||
{ subject: "Health", A: 86, fullMark: 150 },
|
.select("*");
|
||||||
{ subject: "Networking", A: 65, fullMark: 150 },
|
|
||||||
{ subject: "Admin", A: 40, fullMark: 150 },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function AnalyticsPage() {
|
// 2. Fetch projects
|
||||||
const [isExporting, setIsExporting] = useState(false);
|
const { data: projects } = await supabase
|
||||||
const [activeInsight, setActiveInsight] = useState<string | null>(null);
|
.from("projects")
|
||||||
|
.select("*");
|
||||||
|
|
||||||
const handleExport = () => {
|
// 3. Fetch finances
|
||||||
setIsExporting(true);
|
const { data: finances } = await supabase
|
||||||
setTimeout(() => setIsExporting(false), 3000);
|
.from("finance_transactions")
|
||||||
|
.select("*");
|
||||||
|
|
||||||
|
const analyticsData = {
|
||||||
|
tasks: tasks || [],
|
||||||
|
projects: projects || [],
|
||||||
|
finances: finances || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <AnalyticsClient data={analyticsData} />;
|
||||||
<div className="mx-auto max-w-7xl animate-in fade-in slide-in-from-bottom-4 duration-500 h-full flex flex-col text-foreground font-sans space-y-6 pb-12">
|
|
||||||
|
|
||||||
{/* Top Header */}
|
|
||||||
<div className="flex items-center justify-between pb-4 border-b border-white/5 mt-4">
|
|
||||||
<h1 className="text-lg font-medium text-muted-foreground">
|
|
||||||
<span className="text-foreground">Analytics</span> / Performance Metrics
|
|
||||||
</h1>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="bg-[#150F1D] border border-white/5 rounded-sm px-3 py-1.5 flex items-center gap-2">
|
|
||||||
<Filter className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<select className="bg-transparent border-none outline-none text-xs text-foreground cursor-pointer">
|
|
||||||
<option className="bg-[#0A0710]">Last 30 Days</option>
|
|
||||||
<option className="bg-[#0A0710]">Last Quarter</option>
|
|
||||||
<option className="bg-[#0A0710]">This Year</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleExport}
|
|
||||||
disabled={isExporting}
|
|
||||||
className={`min-w-[140px] px-4 py-1.5 rounded-sm text-xs font-semibold flex items-center justify-center gap-2 transition-all ${isExporting ? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30' : 'bg-white/5 hover:bg-white/10 text-foreground border border-white/10'}`}
|
|
||||||
>
|
|
||||||
{isExporting ? (
|
|
||||||
<>
|
|
||||||
<motion.div animate={{ rotate: 360 }} transition={{ repeat: Infinity, duration: 1, ease: "linear" }}>
|
|
||||||
<Download className="h-4 w-4" />
|
|
||||||
</motion.div>
|
|
||||||
GENERATING...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<BarChart3 className="h-4 w-4" />
|
|
||||||
EXPORT REPORT
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* KPI Row */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
|
|
||||||
<KpiCard title="Total Focus Hours" value="124.5" change="+12%" icon={Zap} trend="up" />
|
|
||||||
<KpiCard title="Goal Completion" value="82%" change="+4%" icon={Target} trend="up" color="emerald" />
|
|
||||||
<KpiCard title="Productivity Score" value="9.2" change="-2%" icon={TrendingUp} trend="down" color="blue" />
|
|
||||||
|
|
||||||
{/* Dynamic AI Score Ring */}
|
|
||||||
<div className="rounded-sm border border-primary/20 bg-primary/5 p-6 relative overflow-hidden group flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-[10px] font-bold uppercase tracking-widest text-primary mb-2">Strategic Readiness</h3>
|
|
||||||
<div className="text-3xl font-black text-foreground">94<span className="text-sm font-normal opacity-60 ml-1">%</span></div>
|
|
||||||
<p className="text-[10px] text-muted-foreground mt-2 font-medium">Optimal alignment with goals.</p>
|
|
||||||
</div>
|
|
||||||
<div className="relative w-16 h-16">
|
|
||||||
<svg className="w-full h-full" viewBox="0 0 36 36">
|
|
||||||
<path className="text-white/5" stroke="currentColor" strokeWidth="3" fill="none" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
|
|
||||||
<motion.path
|
|
||||||
initial={{ pathLength: 0 }}
|
|
||||||
animate={{ pathLength: 0.94 }}
|
|
||||||
transition={{ duration: 1.5, ease: "easeOut" }}
|
|
||||||
className="text-primary"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="3"
|
|
||||||
strokeDasharray="100, 100"
|
|
||||||
fill="none"
|
|
||||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<Brain className="h-4 w-4 text-primary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Analysis Area */}
|
|
||||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6 flex-1 min-h-[400px]">
|
|
||||||
|
|
||||||
{/* Productivity Chart with Hotspots */}
|
|
||||||
<div className="xl:col-span-2 rounded-sm border border-white/5 bg-[#0A0710] p-8 flex flex-col relative">
|
|
||||||
<div className="flex justify-between items-start mb-8">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-bold mb-1">Correlation: Focus vs Energy</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">Detailed visual mapping of biological energy impact on focus output.</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-4 text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
||||||
<div className="flex items-center gap-2"><div className="w-2 h-2 rounded-full bg-[#6C5BB0]"></div> Focus</div>
|
|
||||||
<div className="flex items-center gap-2"><div className="w-2 h-2 rounded-full bg-emerald-500"></div> Energy</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Chart Interaction Layer */}
|
|
||||||
<div className="absolute top-24 left-1/2 -translate-x-1/2 z-10 flex gap-4">
|
|
||||||
<button
|
|
||||||
onMouseEnter={() => setActiveInsight("Your energy and focus peaked together on May 20. This suggests your evening rest was highly effective.")}
|
|
||||||
onMouseLeave={() => setActiveInsight(null)}
|
|
||||||
className="p-2 bg-primary/20 border border-primary/40 rounded-full text-primary hover:scale-110 transition-transform animate-pulse"
|
|
||||||
>
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{activeInsight && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: 10 }}
|
|
||||||
className="absolute top-36 left-1/2 -translate-x-1/2 z-20 w-64 p-3 bg-[#1F172B] border border-primary/30 rounded-sm shadow-2xl text-[11px] leading-relaxed text-foreground"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 mb-1 text-primary font-bold uppercase tracking-widest">
|
|
||||||
<Brain className="h-3 w-3" /> AI Observation
|
|
||||||
</div>
|
|
||||||
{activeInsight}
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<div className="flex-1 w-full min-h-[300px]">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<AreaChart data={productivityTrend}>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="colorFocus" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="5%" stopColor="#6C5BB0" stopOpacity={0.4}/>
|
|
||||||
<stop offset="95%" stopColor="#6C5BB0" stopOpacity={0}/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="colorEnergy" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="5%" stopColor="#10b981" stopOpacity={0.4}/>
|
|
||||||
<stop offset="95%" stopColor="#10b981" stopOpacity={0}/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#ffffff05" />
|
|
||||||
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{ fontSize: 11, fill: '#8F89A5' }} dy={10} />
|
|
||||||
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 11, fill: '#8F89A5' }} dx={-10} />
|
|
||||||
<Tooltip
|
|
||||||
cursor={{ stroke: '#6C5BB0', strokeWidth: 1 }}
|
|
||||||
contentStyle={{ backgroundColor: "#150F1D", border: "1px solid rgba(255,255,255,0.05)", fontSize: "12px", color: "#FBF9FE", borderRadius: "4px" }}
|
|
||||||
/>
|
|
||||||
<Area type="monotone" dataKey="focus" stroke="#6C5BB0" strokeWidth={3} fillOpacity={1} fill="url(#colorFocus)" />
|
|
||||||
<Area type="monotone" dataKey="energy" stroke="#10b981" strokeWidth={3} fillOpacity={1} fill="url(#colorEnergy)" />
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sidebar Insights */}
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
|
|
||||||
<div className="rounded-sm border border-white/5 bg-[#0A0710] p-6 flex-1 flex flex-col">
|
|
||||||
<h3 className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-6">Effort Distribution</h3>
|
|
||||||
<div className="flex-1 flex items-center justify-center">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<RadarChart cx="50%" cy="50%" outerRadius="80%" data={focusRadarData}>
|
|
||||||
<PolarGrid stroke="#ffffff05" />
|
|
||||||
<PolarAngleAxis dataKey="subject" tick={{ fill: '#8F89A5', fontSize: 10, fontWeight: 600 }} />
|
|
||||||
<Radar name="Effort" dataKey="A" stroke="#6C5BB0" strokeWidth={2} fill="#6C5BB0" fillOpacity={0.4} />
|
|
||||||
<Tooltip contentStyle={{ backgroundColor: "#1F172B", border: "none", fontSize: "12px" }} />
|
|
||||||
</RadarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-sm border border-white/5 bg-[#0A0710] p-6 flex-1">
|
|
||||||
<h3 className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-6">Strategic Alerts</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-start gap-3 p-3 rounded-sm bg-emerald-500/5 border border-emerald-500/10">
|
|
||||||
<CheckCircle2 className="h-4 w-4 text-emerald-500 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<div className="text-xs font-bold text-emerald-400">Habit Streak Maintained</div>
|
|
||||||
<div className="text-[10px] text-muted-foreground mt-1">Reading streak is now at 12 days. Energy levels are correlating positively.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-3 p-3 rounded-sm bg-orange-500/5 border border-orange-500/10">
|
|
||||||
<AlertCircle className="h-4 w-4 text-orange-500 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<div className="text-xs font-bold text-orange-400">Admin Overload</div>
|
|
||||||
<div className="text-[10px] text-muted-foreground mt-1">Admin tasks have increased by 15%. Consider automating these via Cognis AI.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function KpiCard({ title, value, change, icon: Icon, trend, color = "primary" }: any) {
|
|
||||||
const isUp = trend === "up";
|
|
||||||
const trendColor = isUp ? "text-emerald-500" : "text-rose-500";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-sm border border-white/5 bg-[#0A0710] p-6 relative overflow-hidden group">
|
|
||||||
<div className={`absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity ${color === 'primary' ? 'text-primary' : color === 'emerald' ? 'text-emerald-500' : 'text-blue-500'}`}>
|
|
||||||
<Icon className="h-16 w-16" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-4">{title}</h3>
|
|
||||||
<div className="flex items-end gap-3 mb-1">
|
|
||||||
<span className="text-4xl font-black text-foreground tracking-tight">{value}</span>
|
|
||||||
<span className={`text-[10px] flex items-center font-bold mb-1.5 px-1.5 py-0.5 rounded-sm bg-white/5 ${trendColor}`}>
|
|
||||||
{isUp ? <ArrowUpRight className="h-3 w-3 mr-0.5" /> : <ArrowDownRight className="h-3 w-3 mr-0.5" />} {change}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">vs previous 30 days</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Badge, Button, Card, CardContent } from "poyraz-ui/atoms";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "poyraz-ui/molecules";
|
||||||
|
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis, Line, LineChart } from "recharts";
|
||||||
|
import { CheckCircle2, Wallet, FolderKanban, Activity, CalendarDays, Plus } from "lucide-react";
|
||||||
|
import { QuickActionsSheet } from "@/components/dashboard/quick-actions-sheet";
|
||||||
|
|
||||||
|
export type DashboardData = {
|
||||||
|
tasks: { id: string; status: string; created_at: string; updated_at?: string; due_at?: string }[];
|
||||||
|
projects: { id: string; status: string; name: string }[];
|
||||||
|
finances: { id: string; type: string; amount: number; transaction_date: string }[];
|
||||||
|
logs: { id: string; log_date: string; mood_score: number; energy_score: number }[];
|
||||||
|
events: { id: string; title: string; type: string; starts_at: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type DashboardClientProps = {
|
||||||
|
data: DashboardData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DashboardClient({ data }: DashboardClientProps) {
|
||||||
|
const [dateRange, setDateRange] = useState("this_month");
|
||||||
|
const [isQuickActionOpen, setIsQuickActionOpen] = useState(false);
|
||||||
|
|
||||||
|
// Calculate real metrics from the `data` prop depending on `dateRange`
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 activeProjectsCount = data.projects.filter(p => p.status === "active").length;
|
||||||
|
|
||||||
|
const completedTasksCount = data.tasks.filter(t => t.status === "completed" && filterByDate(t.updated_at || t.created_at)).length;
|
||||||
|
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Format currency
|
||||||
|
const formatCurrency = (val: number) => {
|
||||||
|
return new Intl.NumberFormat("tr-TR", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "USD",
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex max-w-7xl flex-col gap-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Activity className="h-4 w-4" />
|
||||||
|
Genel Bakış
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-semibold tracking-normal text-foreground">
|
||||||
|
Dashboard
|
||||||
|
</h1>
|
||||||
|
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">
|
||||||
|
İş performansını, gelirlerini ve günlük durumunu takip et.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Select value={dateRange} onValueChange={setDateRange}>
|
||||||
|
<SelectTrigger className="w-[160px]">
|
||||||
|
<SelectValue placeholder="Tarih aralığı" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="today">Bugün</SelectItem>
|
||||||
|
<SelectItem value="this_week">Bu Hafta</SelectItem>
|
||||||
|
<SelectItem value="this_month">Bu Ay</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Button onClick={() => setIsQuickActionOpen(true)} className="gap-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Hızlı Ekle
|
||||||
|
</Button>
|
||||||
|
<QuickActionsSheet open={isQuickActionOpen} onOpenChange={setIsQuickActionOpen} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* KPI Cards */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<StatCard label="Net Kazanç" value={formatCurrency(netProfit)} icon={Wallet} tone="green" />
|
||||||
|
<StatCard label="Aktif Projeler" value={activeProjectsCount.toString()} icon={FolderKanban} tone="blue" />
|
||||||
|
<StatCard label="Tamamlanan Görev" value={completedTasksCount.toString()} icon={CheckCircle2} tone="amber" />
|
||||||
|
<StatCard label="Ortalama Mood" value={avgMood} icon={Activity} tone="red" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Charts */}
|
||||||
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<h3 className="mb-6 text-sm font-semibold text-foreground">Gelir / Gider Özeti</h3>
|
||||||
|
<div className="h-[300px] w-full">
|
||||||
|
{incomeTrendData.length > 0 ? (
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart data={incomeTrendData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="name"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
|
||||||
|
dy={10}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
|
||||||
|
dx={-10}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
cursor={{ fill: 'hsl(var(--muted))', opacity: 0.4 }}
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: 'hsl(var(--background))',
|
||||||
|
borderColor: 'hsl(var(--border))',
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="income" fill="hsl(var(--primary))" radius={[4, 4, 0, 0]} />
|
||||||
|
<Bar dataKey="expense" fill="hsl(var(--destructive))" radius={[4, 4, 0, 0]} />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">Bu tarih aralığında finansal veri yok.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<h3 className="mb-6 text-sm font-semibold text-foreground">Mood & Enerji Trendi</h3>
|
||||||
|
<div className="h-[300px] w-full">
|
||||||
|
{moodTrendData.length > 0 ? (
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<LineChart data={moodTrendData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="date"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
|
||||||
|
dy={10}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
domain={[0, 5]}
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
|
||||||
|
width={30}
|
||||||
|
dx={-10}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: 'hsl(var(--background))',
|
||||||
|
borderColor: 'hsl(var(--border))',
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="mood"
|
||||||
|
stroke="hsl(var(--primary))"
|
||||||
|
strokeWidth={3}
|
||||||
|
dot={{ r: 4, fill: "hsl(var(--primary))", strokeWidth: 2, stroke: "hsl(var(--background))" }}
|
||||||
|
activeDot={{ r: 6, strokeWidth: 0 }}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="energy"
|
||||||
|
stroke="#eab308"
|
||||||
|
strokeWidth={3}
|
||||||
|
dot={{ r: 4, fill: "#eab308", strokeWidth: 2, stroke: "hsl(var(--background))" }}
|
||||||
|
activeDot={{ r: 6, strokeWidth: 0 }}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">Bu tarih aralığında günlük verisi yok.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upcoming & Tasks List */}
|
||||||
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground">Yaklaşan Etkinlikler ve Deadlinelar</h3>
|
||||||
|
<CalendarDays className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{data.events.length > 0 ? data.events.map((event) => (
|
||||||
|
<div key={event.id} className="flex items-center gap-3">
|
||||||
|
<div className={`h-2 w-2 rounded-full ${event.type === 'meeting' ? 'bg-primary' : event.type === 'deadline' ? 'bg-destructive' : 'bg-amber-500'}`} />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium">{event.title}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{new Date(event.starts_at).toLocaleDateString("tr-TR", { month: "long", day: "numeric", hour: "2-digit", minute: "2-digit" })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)) : (
|
||||||
|
<div className="text-sm text-muted-foreground py-4 text-center border border-dashed rounded-sm border-border bg-muted/20">Yaklaşan etkinlik yok.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatCard({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
icon: Icon,
|
||||||
|
tone,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
icon: typeof FolderKanban;
|
||||||
|
tone: "green" | "blue" | "amber" | "red";
|
||||||
|
}) {
|
||||||
|
const toneClass = {
|
||||||
|
green: "bg-emerald-50 text-emerald-700",
|
||||||
|
blue: "bg-blue-50 text-blue-700",
|
||||||
|
amber: "bg-amber-50 text-amber-700",
|
||||||
|
red: "bg-primary/10 text-primary",
|
||||||
|
}[tone];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex items-center justify-between gap-3 p-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">{label}</p>
|
||||||
|
<p className="mt-1 text-2xl font-semibold text-foreground">{value}</p>
|
||||||
|
</div>
|
||||||
|
<div className={`flex h-10 w-10 items-center justify-center rounded-sm ${toneClass}`}>
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
+56
-382
@@ -1,395 +1,69 @@
|
|||||||
"use client";
|
import { createClient } from "@/lib/supabase/server";
|
||||||
|
import { DashboardClient } from "./dashboard-client";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { useState } from "react";
|
export const metadata = {
|
||||||
import {
|
title: "Dashboard - Cognis",
|
||||||
Bar,
|
};
|
||||||
BarChart,
|
|
||||||
CartesianGrid,
|
|
||||||
Cell,
|
|
||||||
Line,
|
|
||||||
LineChart,
|
|
||||||
Pie,
|
|
||||||
PieChart,
|
|
||||||
ResponsiveContainer,
|
|
||||||
Tooltip,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
} from "recharts";
|
|
||||||
import {
|
|
||||||
CheckSquare2, Droplets, Moon, Target, Activity,
|
|
||||||
MoreHorizontal, PenTool, Plus, GripVertical, Maximize2
|
|
||||||
} from "lucide-react";
|
|
||||||
import { motion, Reorder } from "framer-motion";
|
|
||||||
import { QuickActionsSheet } from "@/components/dashboard/quick-actions-sheet";
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
||||||
|
|
||||||
// Mock Data
|
export default async function DashboardPage() {
|
||||||
const focusTrendData = [
|
const supabase = await createClient();
|
||||||
{ date: "May 12", value: 55 },
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
{ date: "May 13", value: 42 },
|
|
||||||
{ date: "May 14", value: 60 },
|
|
||||||
{ date: "May 15", value: 85 },
|
|
||||||
{ date: "May 16", value: 72 },
|
|
||||||
{ date: "May 17", value: 87 },
|
|
||||||
{ date: "May 18", value: 100 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const sleepData = [
|
if (!user) {
|
||||||
{ day: "M", value: 6.5 },
|
redirect("/login");
|
||||||
{ day: "T", value: 7.2 },
|
}
|
||||||
{ day: "W", value: 5.8 },
|
|
||||||
{ day: "T", value: 6.1 },
|
|
||||||
{ day: "F", value: 8.0 },
|
|
||||||
{ day: "S", value: 7.5 },
|
|
||||||
{ day: "S", value: 8.5 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const waterData = [
|
// Get current date and date 30 days ago
|
||||||
{ day: "M", value: 1.5 },
|
const today = new Date();
|
||||||
{ day: "T", value: 2.1 },
|
const thirtyDaysAgo = new Date();
|
||||||
{ day: "W", value: 1.8 },
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||||
{ day: "T", value: 2.5 },
|
|
||||||
{ day: "F", value: 2.0 },
|
|
||||||
{ day: "S", value: 3.0 },
|
|
||||||
{ day: "S", value: 2.8 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const moodData = [
|
|
||||||
{ day: "M", value: 60 },
|
|
||||||
{ day: "T", value: 85 },
|
|
||||||
{ day: "W", value: 70 },
|
|
||||||
{ day: "T", value: 65 },
|
|
||||||
{ day: "F", value: 80 },
|
|
||||||
{ day: "S", value: 90 },
|
|
||||||
{ day: "S", value: 95 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const focusDistributionData = [
|
|
||||||
{ name: "Deep Work", value: 45, color: "#6C5BB0" },
|
|
||||||
{ name: "Shallow Work", value: 30, color: "#54468B" },
|
|
||||||
{ name: "Meetings", value: 15, color: "#3B3161" },
|
|
||||||
{ name: "Breaks", value: 10, color: "#221C38" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const upcomingEvents = [
|
|
||||||
{ time: "10:00 AM", title: "Deep Work Session", category: "FOCUS", color: "border-l-pink-500" },
|
|
||||||
{ time: "12:30 PM", title: "Team Standup", category: "MEETING", color: "border-l-orange-500" },
|
|
||||||
{ time: "02:00 PM", title: "Project Review", category: "WORK", color: "border-l-blue-500" },
|
|
||||||
{ time: "04:00 PM", title: "Workout", category: "HEALTH", color: "border-l-emerald-500" },
|
|
||||||
{ time: "07:00 PM", title: "Daily Reflection", category: "PERSONAL", color: "border-l-purple-500" },
|
|
||||||
];
|
|
||||||
|
|
||||||
type WidgetId = "sleep" | "water" | "mood" | "distribution";
|
|
||||||
|
|
||||||
export default function DashboardPage() {
|
|
||||||
const [isQuickActionOpen, setIsQuickActionOpen] = useState(false);
|
|
||||||
const [selectedKpi, setSelectedKpi] = useState<WidgetId | null>(null);
|
|
||||||
|
|
||||||
// Widget ordering state
|
const todayStr = today.toISOString();
|
||||||
const [widgetOrder, setWidgetOrder] = useState<WidgetId[]>(["sleep", "water", "mood", "distribution"]);
|
const thirtyDaysAgoStr = thirtyDaysAgo.toISOString();
|
||||||
|
|
||||||
return (
|
// 1. Fetch tasks
|
||||||
<div className="mx-auto max-w-7xl animate-in fade-in slide-in-from-bottom-4 duration-500 space-y-6 text-foreground font-sans pb-12">
|
const { data: tasks } = await supabase
|
||||||
|
.from("tasks")
|
||||||
{/* Top Header */}
|
.select("*")
|
||||||
<div className="flex items-center justify-between pb-4 border-b border-white/5 mb-6 mt-4">
|
.or(`due_at.gte.${thirtyDaysAgoStr},created_at.gte.${thirtyDaysAgoStr}`)
|
||||||
<h1 className="text-lg font-medium text-muted-foreground">
|
.order("due_at", { ascending: true });
|
||||||
<span className="text-foreground">Dashboard</span> / Overview
|
|
||||||
</h1>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsQuickActionOpen(true)}
|
|
||||||
className="bg-primary hover:bg-primary/90 text-primary-foreground border border-primary/20 px-4 py-1.5 rounded-sm text-xs font-semibold flex items-center gap-2 transition-all active:scale-95 shadow-lg shadow-primary/20"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
QUICK ADD
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
// 2. Fetch projects
|
||||||
|
const { data: projects } = await supabase
|
||||||
{/* LEFT COLUMN: Focus Trend (Takes 2 columns on XL) */}
|
.from("projects")
|
||||||
<div className="xl:col-span-2 flex flex-col gap-6">
|
.select("*")
|
||||||
<div className="group rounded-sm border border-white/5 bg-[#0A0710] p-6 flex-1 min-h-[400px] relative">
|
.order("created_at", { ascending: false });
|
||||||
<div className="flex justify-between items-start mb-8">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-4">Focus Trend</h3>
|
|
||||||
<div className="text-5xl font-bold mb-1 text-primary">87<span className="text-2xl">%</span></div>
|
|
||||||
<div className="text-sm text-muted-foreground">Average Focus Score</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<select className="bg-transparent border border-white/10 rounded-sm text-xs px-3 py-1.5 outline-none focus:border-primary/50 cursor-pointer">
|
|
||||||
<option className="bg-[#0A0710]">7 DAYS</option>
|
|
||||||
<option className="bg-[#0A0710]">14 DAYS</option>
|
|
||||||
<option className="bg-[#0A0710]">30 DAYS</option>
|
|
||||||
</select>
|
|
||||||
<button className="p-1.5 border border-white/10 rounded-sm hover:bg-white/5 transition-colors">
|
|
||||||
<Activity className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-[280px] w-full">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<LineChart data={focusTrendData}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#ffffff10" />
|
|
||||||
<XAxis
|
|
||||||
dataKey="date"
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fontSize: 11, fill: '#8F89A5' }}
|
|
||||||
dy={10}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
domain={[0, 100]}
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fontSize: 11, fill: '#8F89A5' }}
|
|
||||||
width={30}
|
|
||||||
dx={-10}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
contentStyle={{ backgroundColor: "#1F172B", borderColor: "#3B3448", borderRadius: "4px", fontSize: "12px" }}
|
|
||||||
itemStyle={{ color: "#6C5BB0" }}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
type="monotone"
|
|
||||||
dataKey="value"
|
|
||||||
stroke="#6C5BB0"
|
|
||||||
strokeWidth={3}
|
|
||||||
dot={{ r: 4, fill: "#1F172B", strokeWidth: 2 }}
|
|
||||||
activeDot={{ r: 6, fill: "#6C5BB0", stroke: "#1F172B" }}
|
|
||||||
/>
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* RIGHT COLUMN: Quick Add & Upcoming */}
|
// 3. Fetch finances
|
||||||
<div className="flex flex-col gap-6">
|
const { data: finances } = await supabase
|
||||||
|
.from("finance_transactions")
|
||||||
{/* AI Strategy Suggestion (Replacing Quick Add Input) */}
|
.select("*")
|
||||||
<div className="rounded-sm border border-primary/20 bg-[#1F172B] p-6 relative overflow-hidden group">
|
.gte("transaction_date", thirtyDaysAgoStr)
|
||||||
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-30 transition-opacity">
|
.order("transaction_date", { ascending: true });
|
||||||
<Activity className="h-24 w-24 text-primary" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-primary mb-4 flex items-center gap-2">
|
|
||||||
<Activity className="h-3 w-3" />
|
|
||||||
AI Insight
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm font-medium leading-relaxed mb-4">
|
|
||||||
"Based on your 100% focus score yesterday, you have high mental momentum. Schedule your hardest task for 10:00 AM today."
|
|
||||||
</p>
|
|
||||||
<button className="text-[10px] font-bold uppercase tracking-widest text-primary border-b border-primary/50 pb-0.5 hover:border-primary transition-all">
|
|
||||||
Apply Suggestion
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Upcoming */}
|
// 4. Fetch daily logs
|
||||||
<div className="rounded-sm border border-white/5 bg-[#0A0710] p-6 flex-1">
|
const { data: logs } = await supabase
|
||||||
<div className="flex justify-between items-center mb-6">
|
.from("daily_logs")
|
||||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Upcoming</h3>
|
.select("*")
|
||||||
<button className="text-[10px] uppercase font-semibold text-muted-foreground hover:text-foreground">View All</button>
|
.gte("log_date", thirtyDaysAgoStr)
|
||||||
</div>
|
.order("log_date", { ascending: true });
|
||||||
<div className="space-y-4">
|
|
||||||
{upcomingEvents.map((event, idx) => (
|
|
||||||
<div key={idx} className="flex items-center text-sm py-1">
|
|
||||||
<div className={`w-1 h-10 ${event.color} border-l-2 mr-4 rounded-full`}></div>
|
|
||||||
<div className="w-20 text-muted-foreground text-xs">{event.time}</div>
|
|
||||||
<div className="flex-1 font-medium">{event.title}</div>
|
|
||||||
<div className="text-[10px] uppercase font-semibold text-muted-foreground tracking-wider">{event.category}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
// 5. Fetch calendar events (upcoming)
|
||||||
</div>
|
const { data: events } = await supabase
|
||||||
|
.from("calendar_events")
|
||||||
|
.select("*")
|
||||||
|
.gte("starts_at", todayStr)
|
||||||
|
.order("starts_at", { ascending: true })
|
||||||
|
.limit(10);
|
||||||
|
|
||||||
{/* BOTTOM ROW: Draggable Widgets */}
|
const dashboardData = {
|
||||||
<Reorder.Group
|
tasks: tasks || [],
|
||||||
axis="x"
|
projects: projects || [],
|
||||||
values={widgetOrder}
|
finances: finances || [],
|
||||||
onReorder={setWidgetOrder}
|
logs: logs || [],
|
||||||
className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6"
|
events: events || [],
|
||||||
>
|
};
|
||||||
{widgetOrder.map((id) => (
|
|
||||||
<Reorder.Item
|
|
||||||
key={id}
|
|
||||||
value={id}
|
|
||||||
className="cursor-default"
|
|
||||||
>
|
|
||||||
{id === "sleep" && (
|
|
||||||
<WidgetCard
|
|
||||||
title="Sleep Hours"
|
|
||||||
icon={Moon}
|
|
||||||
value="7.2"
|
|
||||||
unit="HOURS"
|
|
||||||
goal="7-8 Hours"
|
|
||||||
data={sleepData}
|
|
||||||
onExpand={() => setSelectedKpi("sleep")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{id === "water" && (
|
|
||||||
<WidgetCard
|
|
||||||
title="Water Intake"
|
|
||||||
icon={Droplets}
|
|
||||||
value="2.1"
|
|
||||||
unit="LITERS"
|
|
||||||
goal="2.5L"
|
|
||||||
data={waterData}
|
|
||||||
onExpand={() => setSelectedKpi("water")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{id === "mood" && (
|
|
||||||
<WidgetCard
|
|
||||||
title="Mood Score"
|
|
||||||
icon={Activity}
|
|
||||||
value="78"
|
|
||||||
unit="/100"
|
|
||||||
goal="Positive"
|
|
||||||
data={moodData}
|
|
||||||
onExpand={() => setSelectedKpi("mood")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{id === "distribution" && (
|
|
||||||
<div className="rounded-sm border border-white/5 bg-[#0A0710] p-6 h-full flex flex-col group relative">
|
|
||||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
|
|
||||||
<div className="p-1 hover:bg-white/5 rounded-sm cursor-grab active:cursor-grabbing">
|
|
||||||
<GripVertical className="h-3 w-3 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-6">Focus Distribution</h3>
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
|
||||||
<div className="w-[100px] h-[100px] relative shrink-0">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<PieChart>
|
|
||||||
<Pie
|
|
||||||
data={focusDistributionData}
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
innerRadius={35}
|
|
||||||
outerRadius={50}
|
|
||||||
paddingAngle={2}
|
|
||||||
dataKey="value"
|
|
||||||
stroke="none"
|
|
||||||
>
|
|
||||||
{focusDistributionData.map((entry, index) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 space-y-2">
|
|
||||||
{focusDistributionData.slice(0, 3).map((item, idx) => (
|
|
||||||
<div key={idx} className="flex justify-between items-center text-[10px]">
|
|
||||||
<div className="flex items-center gap-1.5 truncate mr-2">
|
|
||||||
<div className="w-1.5 h-1.5 rounded-full shrink-0" style={{ backgroundColor: item.color }}></div>
|
|
||||||
<span className="text-muted-foreground truncate">{item.name}</span>
|
|
||||||
</div>
|
|
||||||
<span className="font-semibold">{item.value}%</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Reorder.Item>
|
|
||||||
))}
|
|
||||||
</Reorder.Group>
|
|
||||||
|
|
||||||
{/* Quote Footer */}
|
return <DashboardClient data={dashboardData} />;
|
||||||
<div className="rounded-sm border border-white/5 bg-[#0A0710] p-6 flex justify-between items-center group">
|
|
||||||
<div className="flex gap-4 items-center">
|
|
||||||
<div className="bg-[#150F1D] text-muted-foreground border border-white/5 p-2 rounded-sm group-hover:text-primary transition-colors">
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-[10px] font-semibold tracking-wider text-muted-foreground mb-1">SYSTEM REMINDER</div>
|
|
||||||
<div className="text-sm">"Discipline is the bridge between goals and accomplishment."</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground italic">- Jim Rohn</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sheet & Modals */}
|
|
||||||
<QuickActionsSheet
|
|
||||||
isOpen={isQuickActionOpen}
|
|
||||||
onClose={() => setIsQuickActionOpen(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Dialog open={!!selectedKpi} onOpenChange={() => setSelectedKpi(null)}>
|
|
||||||
<DialogContent className="bg-[#0A0710] border-white/5 max-w-2xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="text-xl font-bold flex items-center gap-2">
|
|
||||||
{selectedKpi === "sleep" && <><Moon className="h-5 w-5 text-primary" /> Sleep Analysis</>}
|
|
||||||
{selectedKpi === "water" && <><Droplets className="h-5 w-5 text-primary" /> Hydration Trends</>}
|
|
||||||
{selectedKpi === "mood" && <><Activity className="h-5 w-5 text-primary" /> Mood Tracking</>}
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="h-[300px] w-full mt-6">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<BarChart data={selectedKpi === "sleep" ? sleepData : selectedKpi === "water" ? waterData : moodData}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#ffffff10" />
|
|
||||||
<XAxis dataKey="day" axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: '#8F89A5' }} />
|
|
||||||
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: '#8F89A5' }} />
|
|
||||||
<Tooltip
|
|
||||||
cursor={{ fill: '#ffffff05' }}
|
|
||||||
contentStyle={{ backgroundColor: "#1F172B", border: "none", borderRadius: "4px" }}
|
|
||||||
/>
|
|
||||||
<Bar dataKey="value" fill="#6C5BB0" radius={[4, 4, 0, 0]} />
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 grid grid-cols-2 gap-4">
|
|
||||||
<div className="p-4 rounded-sm border border-white/5 bg-[#150F1D]">
|
|
||||||
<h4 className="text-[10px] font-bold text-muted-foreground uppercase mb-2">AI Observation</h4>
|
|
||||||
<p className="text-sm">Your consistency is up 12% from last week. Keep this pace for optimal performance.</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 rounded-sm border border-white/5 bg-[#150F1D]">
|
|
||||||
<h4 className="text-[10px] font-bold text-muted-foreground uppercase mb-2">Recommendation</h4>
|
|
||||||
<p className="text-sm">Try to reach your goal by increasing focus in the evening blocks.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function WidgetCard({ title, icon: Icon, value, unit, goal, data, onExpand }: any) {
|
|
||||||
return (
|
|
||||||
<div className="rounded-sm border border-white/5 bg-[#0A0710] p-6 h-full flex flex-col justify-between group relative">
|
|
||||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
|
|
||||||
<button onClick={onExpand} className="p-1 hover:bg-white/5 rounded-sm transition-colors">
|
|
||||||
<Maximize2 className="h-3 w-3 text-muted-foreground" />
|
|
||||||
</button>
|
|
||||||
<div className="p-1 hover:bg-white/5 rounded-sm cursor-grab active:cursor-grabbing">
|
|
||||||
<GripVertical className="h-3 w-3 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-start mb-6">
|
|
||||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">{title}</h3>
|
|
||||||
<Icon className="h-4 w-4 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex items-baseline gap-1 mb-6">
|
|
||||||
<span className="text-4xl font-bold">{value}</span>
|
|
||||||
<span className="text-[10px] text-muted-foreground tracking-wider">{unit}</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-[80px] w-full">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<BarChart data={data} margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
|
|
||||||
<XAxis dataKey="day" hide />
|
|
||||||
<Tooltip cursor={{ fill: '#ffffff05' }} contentStyle={{ backgroundColor: "#1F172B", border: "none", fontSize: "12px" }} />
|
|
||||||
<Bar dataKey="value" fill="#6C5BB0" radius={[2, 2, 0, 0]} />
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 pt-4 border-t border-white/5 text-[11px] text-primary">Goal: {goal}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user