215 lines
8.2 KiB
TypeScript
215 lines
8.2 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { University1415Dimension } from '../types/futures';
|
||
import { SpiderChart } from '../components/visual/SpiderChart';
|
||
import { PageHeader } from '../components/common/PageHeader';
|
||
import { StatCard } from '../components/common/StatCard';
|
||
import { toPersianDigits } from '../utils/persianNumbers';
|
||
import { HORIZON_END } from '../utils/horizon';
|
||
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||
import { Progress } from '@/components/ui/progress';
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow
|
||
} from '@/components/ui/table';
|
||
import { Target, AlertTriangle, Award } from 'lucide-react';
|
||
|
||
interface GapAnalysisPageProps {
|
||
dimensions: University1415Dimension[];
|
||
onSelectDimension: (dim: University1415Dimension) => void;
|
||
}
|
||
|
||
export const GapAnalysisPage: React.FC<GapAnalysisPageProps> = ({
|
||
dimensions,
|
||
onSelectDimension
|
||
}) => {
|
||
const [tab, setTab] = useState<'spider' | 'table'>('spider');
|
||
|
||
if (!dimensions.length) return null;
|
||
|
||
const byGap = [...dimensions].sort((a, b) => b.gap - a.gap);
|
||
const byScore = [...dimensions].sort((a, b) => b.currentScore - a.currentScore);
|
||
|
||
const avgReadiness = Math.round(
|
||
dimensions.reduce((acc, d) => acc + d.readinessPercentage, 0) / dimensions.length
|
||
);
|
||
|
||
const widestGap = byGap[0];
|
||
const strongest = byScore[0];
|
||
|
||
return (
|
||
<div className="animate-in fade-in duration-300">
|
||
<PageHeader
|
||
title="تحلیل شکاف"
|
||
description={`فاصله وضع موجود دانشگاه تا هدف ${toPersianDigits(
|
||
HORIZON_END
|
||
)} در ۱۰ ویژگی.`}
|
||
actions={
|
||
<Tabs value={tab} onValueChange={(v) => setTab(v as typeof tab)}>
|
||
<TabsList className="h-9">
|
||
<TabsTrigger value="spider">نمودار عنکبوتی</TabsTrigger>
|
||
<TabsTrigger value="table">جدول</TabsTrigger>
|
||
</TabsList>
|
||
</Tabs>
|
||
}
|
||
/>
|
||
|
||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||
<StatCard
|
||
label="آمادگی کل"
|
||
value={`${toPersianDigits(avgReadiness)}٪`}
|
||
meta={`فاصله تا هدف ${toPersianDigits(100 - avgReadiness)}٪`}
|
||
icon={Target}
|
||
accent="var(--primary)"
|
||
/>
|
||
<StatCard
|
||
label="بزرگترین شکاف"
|
||
value={toPersianDigits(widestGap.gap)}
|
||
meta={widestGap.title}
|
||
icon={AlertTriangle}
|
||
accent="var(--negative)"
|
||
onClick={() => onSelectDimension(widestGap)}
|
||
/>
|
||
<StatCard
|
||
label="دومین شکاف"
|
||
value={toPersianDigits(byGap[1].gap)}
|
||
meta={byGap[1].title}
|
||
icon={AlertTriangle}
|
||
accent="var(--cat-policy)"
|
||
onClick={() => onSelectDimension(byGap[1])}
|
||
/>
|
||
<StatCard
|
||
label="قویترین بعد"
|
||
value={`${toPersianDigits(strongest.currentScore)}/۱۰`}
|
||
meta={strongest.title}
|
||
icon={Award}
|
||
accent="var(--positive)"
|
||
onClick={() => onSelectDimension(strongest)}
|
||
/>
|
||
</div>
|
||
|
||
{tab === 'spider' && (
|
||
<div className="mt-4 grid animate-in grid-cols-1 gap-4 fade-in duration-200 xl:grid-cols-12">
|
||
<Card data-card className="xl:col-span-8">
|
||
<CardContent className="flex items-center justify-center p-3 sm:p-6">
|
||
<SpiderChart
|
||
dimensions={dimensions}
|
||
onSelectDimension={onSelectDimension}
|
||
height={540}
|
||
/>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card data-card className="xl:col-span-4">
|
||
<CardHeader className="border-b pb-3">
|
||
<CardTitle className="text-[14px]">رتبهبندی شکافها</CardTitle>
|
||
<p className="text-[11.5px] text-muted-foreground">
|
||
از بیشترین فاصله تا کمترین.
|
||
</p>
|
||
</CardHeader>
|
||
<CardContent className="pt-2">
|
||
<ul className="divide-y">
|
||
{byGap.map((dim, idx) => (
|
||
<li key={dim.id}>
|
||
<button
|
||
onClick={() => onSelectDimension(dim)}
|
||
className="group flex w-full items-center gap-2.5 py-2 text-right"
|
||
>
|
||
<span className="tnum w-5 shrink-0 text-[11px] text-muted-foreground">
|
||
{toPersianDigits(idx + 1)}
|
||
</span>
|
||
<span className="min-w-0 flex-1">
|
||
<span className="block truncate text-[12.5px] font-semibold transition-colors group-hover:text-primary">
|
||
{dim.shortTag}
|
||
</span>
|
||
<Progress
|
||
value={dim.readinessPercentage}
|
||
className="mt-1 h-1.5"
|
||
/>
|
||
</span>
|
||
<span
|
||
className="tnum shrink-0 rounded px-1.5 py-0.5 text-[11px] font-semibold"
|
||
style={{
|
||
background: 'var(--negative-soft)',
|
||
color: 'var(--negative)'
|
||
}}
|
||
>
|
||
{toPersianDigits(dim.gap)}
|
||
</span>
|
||
</button>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)}
|
||
|
||
{tab === 'table' && (
|
||
<Card data-card className="mt-4 animate-in overflow-hidden p-0 fade-in duration-200">
|
||
<div className="overflow-x-auto">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead className="text-right">ویژگی</TableHead>
|
||
<TableHead className="text-right">موجود</TableHead>
|
||
<TableHead className="text-right">هدف</TableHead>
|
||
<TableHead className="text-right">شکاف</TableHead>
|
||
<TableHead className="text-right">آمادگی</TableHead>
|
||
<TableHead className="text-right">اقدام پیشنهادی</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{dimensions.map((dim) => (
|
||
<TableRow
|
||
key={dim.id}
|
||
onClick={() => onSelectDimension(dim)}
|
||
className="cursor-pointer"
|
||
>
|
||
<TableCell className="font-semibold">
|
||
<span className="flex items-center gap-2">
|
||
<span
|
||
className="cat-dot"
|
||
style={{ background: dim.pillarColor }}
|
||
/>
|
||
{dim.title}
|
||
</span>
|
||
</TableCell>
|
||
<TableCell className="tnum">
|
||
{toPersianDigits(dim.currentScore)}
|
||
</TableCell>
|
||
<TableCell className="tnum text-primary">
|
||
{toPersianDigits(dim.targetScore)}
|
||
</TableCell>
|
||
<TableCell className="tnum font-semibold text-negative">
|
||
{toPersianDigits(dim.gap)}
|
||
</TableCell>
|
||
<TableCell>
|
||
<span className="flex items-center gap-2">
|
||
<Progress value={dim.readinessPercentage} className="h-1.5 w-16" />
|
||
<span className="tnum text-[11.5px]">
|
||
{toPersianDigits(dim.readinessPercentage)}٪
|
||
</span>
|
||
</span>
|
||
</TableCell>
|
||
<TableCell className="max-w-sm text-muted-foreground">
|
||
<span className="line-clamp-1">
|
||
{dim.strategicRecommendations[0]}
|
||
</span>
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|