Some checks failed
Deploy to Cloudflare Workers / deploy (push) Has been cancelled
251 lines
8.3 KiB
TypeScript
251 lines
8.3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { api } from "@/lib/api/client";
|
|
import { PageHeader } from "@/components/dashboard/page-header";
|
|
import { StatCard } from "@/components/dashboard/stat-card";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Calendar,
|
|
Target,
|
|
TrendingUp,
|
|
Trophy,
|
|
ArrowUpRight,
|
|
} from "lucide-react";
|
|
import { FormDonutChart, GoalsTrendChart } from "@/components/manager/TeamCharts";
|
|
|
|
export function ManagerDashboardClient() {
|
|
const [activeTab, setActiveTab] = useState("overview");
|
|
const [data, setData] = useState<{
|
|
nextFixture: Record<string, unknown> | null;
|
|
recentResults: Record<string, unknown>[];
|
|
stats: {
|
|
wins: number;
|
|
draws: number;
|
|
losses: number;
|
|
goalsFor: number;
|
|
played: number;
|
|
} | null;
|
|
} | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
api.manager.dashboard().then(setData).finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
const stats = data?.stats;
|
|
const results = (data?.recentResults ?? []) as {
|
|
result: string;
|
|
goals_for: number;
|
|
goals_against: number;
|
|
opponent_name: string;
|
|
scheduled_at: string;
|
|
}[];
|
|
|
|
const winRate =
|
|
stats && stats.played > 0
|
|
? Math.round((stats.wins / stats.played) * 100)
|
|
: 0;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<PageHeader
|
|
title="Dashboard"
|
|
description="Result analysis and your next fixture"
|
|
tabs={[
|
|
{ id: "overview", label: "Overview" },
|
|
{ id: "reports", label: "Reports" },
|
|
{ id: "activity", label: "Activity" },
|
|
]}
|
|
activeTab={activeTab}
|
|
onTabChange={setActiveTab}
|
|
actions={
|
|
<Button variant="outline" size="sm" asChild>
|
|
<Link href="/manager/leagues">
|
|
View leagues
|
|
<ArrowUpRight className="h-3.5 w-3.5" />
|
|
</Link>
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
{loading ? (
|
|
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
{[1, 2, 3, 4].map((i) => (
|
|
<Card key={i} className="h-32 animate-pulse bg-muted/30" />
|
|
))}
|
|
</div>
|
|
) : activeTab === "overview" ? (
|
|
<>
|
|
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
<StatCard
|
|
title="Matches played"
|
|
value={stats?.played ?? 0}
|
|
subtitle="Last 8 results"
|
|
icon={TrendingUp}
|
|
trend={stats && stats.wins > stats.losses ? "up" : "neutral"}
|
|
trendLabel={`${stats?.wins ?? 0}W ${stats?.draws ?? 0}D ${stats?.losses ?? 0}L`}
|
|
/>
|
|
<StatCard
|
|
title="Goals scored"
|
|
value={stats?.goalsFor ?? 0}
|
|
subtitle="Recent window"
|
|
icon={Target}
|
|
trend="up"
|
|
trendLabel="attacking"
|
|
/>
|
|
<StatCard
|
|
title="Win rate"
|
|
value={`${winRate}%`}
|
|
subtitle="From recent form"
|
|
icon={Trophy}
|
|
trend={winRate >= 50 ? "up" : "down"}
|
|
trendLabel="form"
|
|
/>
|
|
<StatCard
|
|
title="Next fixture"
|
|
value={data?.nextFixture ? "Ready" : "—"}
|
|
subtitle={data?.nextFixture ? "See below" : "None scheduled"}
|
|
icon={Calendar}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-4 lg:grid-cols-7">
|
|
<Card className="lg:col-span-4">
|
|
<CardHeader>
|
|
<CardTitle>Result analysis</CardTitle>
|
|
<CardDescription>Form and goals trend</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{results.length > 0 ? (
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<FormDonutChart
|
|
data={[
|
|
{ name: "W", value: stats?.wins ?? 0 },
|
|
{ name: "D", value: stats?.draws ?? 0 },
|
|
{ name: "L", value: stats?.losses ?? 0 },
|
|
].filter((d) => d.value > 0)}
|
|
/>
|
|
<GoalsTrendChart
|
|
data={results.map((r, i) => ({
|
|
matchday: i + 1,
|
|
gf: r.goals_for,
|
|
ga: r.goals_against,
|
|
}))}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">
|
|
Complete matches to unlock charts.
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="lg:col-span-3">
|
|
<CardHeader>
|
|
<CardTitle>Next fixture check</CardTitle>
|
|
<CardDescription>Upcoming match</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{data?.nextFixture ? (
|
|
<NextFixtureCard fixture={data.nextFixture} />
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">
|
|
No upcoming fixtures. Check{" "}
|
|
<Link
|
|
href="/manager/leagues"
|
|
className="text-foreground underline-offset-4 hover:underline"
|
|
>
|
|
leagues
|
|
</Link>
|
|
.
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Recent results</CardTitle>
|
|
<CardDescription>Latest match outcomes</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
{results.map((r, i) => (
|
|
<div
|
|
key={i}
|
|
className="flex items-center justify-between rounded-lg border border-border bg-muted/30 px-4 py-3 text-sm"
|
|
>
|
|
<span className="font-medium">vs {r.opponent_name}</span>
|
|
<div className="flex items-center gap-2">
|
|
<span className="tabular-nums text-muted-foreground">
|
|
{r.goals_for}-{r.goals_against}
|
|
</span>
|
|
<Badge
|
|
variant={
|
|
r.result === "W"
|
|
? "success"
|
|
: r.result === "L"
|
|
? "destructive"
|
|
: "warning"
|
|
}
|
|
>
|
|
{r.result}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{results.length === 0 && (
|
|
<p className="text-sm text-muted-foreground">No results yet</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</>
|
|
) : (
|
|
<Card>
|
|
<CardContent className="py-12 text-center text-sm text-muted-foreground">
|
|
{activeTab === "reports"
|
|
? "Detailed reports coming soon."
|
|
: "Activity feed coming soon."}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function NextFixtureCard({ fixture }: { fixture: Record<string, unknown> }) {
|
|
const home = fixture.home as { name: string } | null;
|
|
const away = fixture.away as { name: string } | null;
|
|
const when =
|
|
(fixture.scheduled_at as string) ||
|
|
(fixture.proposed_scheduled_at as string);
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<p className="text-lg font-semibold">
|
|
{home?.name} <span className="text-muted-foreground">vs</span>{" "}
|
|
{away?.name}
|
|
</p>
|
|
{when && (
|
|
<p className="text-sm text-muted-foreground">
|
|
{new Date(when).toLocaleString()}
|
|
</p>
|
|
)}
|
|
<Badge variant="outline" className="capitalize">
|
|
{(fixture.status as string)?.replace(/_/g, " ")}
|
|
</Badge>
|
|
</div>
|
|
);
|
|
}
|