uniradar/src/pages/GapAnalysisPage.tsx

215 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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>
);
};