uniradar/src/pages/ReportsPage.tsx

184 lines
7.4 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 from 'react';
import { IntelligenceItem, University1415Dimension, JobEntity } from '../types/futures';
import { PageHeader } from '../components/common/PageHeader';
import { toPersianDigits, getHorizonLabelFa } from '../utils/persianNumbers';
import { CATEGORY_META, CATEGORY_ORDER } from '../utils/categories';
import { HORIZON_START, HORIZON_END } from '../utils/horizon';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
import { Printer } from 'lucide-react';
interface ReportsPageProps {
items: IntelligenceItem[];
dimensions: University1415Dimension[];
jobs: JobEntity[];
}
export const ReportsPage: React.FC<ReportsPageProps> = ({ items, dimensions, jobs }) => {
const criticalItems = items.filter(
(i) => i.status === 'critical' || i.impactScore === 5
);
const byGap = [...dimensions].sort((a, b) => b.gap - a.gap);
const avgReadiness = dimensions.length
? Math.round(
dimensions.reduce((acc, d) => acc + d.readinessPercentage, 0) / dimensions.length
)
: 0;
const growingJobs = jobs.filter((j) => j.type === 'growing').length;
return (
<div className="mx-auto max-w-5xl animate-in fade-in duration-300">
<div className="no-print">
<PageHeader
title="گزارش‌ها"
description="خلاصه وضعیت رصد و شکاف‌ها، آماده چاپ."
actions={
<Button size="sm" onClick={() => window.print()}>
<Printer />
چاپ
</Button>
}
/>
</div>
<Card data-card className="p-6 sm:p-8">
{/* Document header */}
<header className="mb-6 border-b pb-5">
<h2 className="text-lg font-bold">
رادار آینده دانشگاه اصفهان گزارش وضعیت
</h2>
<p className="tnum mt-1 text-[12px] text-muted-foreground">
افق راهبردی {toPersianDigits(HORIZON_START)} تا {toPersianDigits(HORIZON_END)}
</p>
</header>
{/* Figures, all derived from the dataset. */}
<section className="mb-7">
<SectionTitle>در یک نگاه</SectionTitle>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<Figure label="موارد رصدشده" value={toPersianDigits(items.length)} />
<Figure label="موارد بحرانی" value={toPersianDigits(criticalItems.length)} />
<Figure label="آمادگی ۱۴۱۵" value={`${toPersianDigits(avgReadiness)}٪`} />
<Figure label="مشاغل رو به رشد" value={toPersianDigits(growingJobs)} />
</div>
<div className="mt-3 grid grid-cols-2 gap-3 sm:grid-cols-4">
{CATEGORY_ORDER.map((cat) => {
const meta = CATEGORY_META[cat];
const count = items.filter((i) => i.category === cat).length;
return (
<div key={cat} className="rounded-lg border p-3">
<div className="flex items-center gap-1.5 text-[11.5px] text-muted-foreground">
<span className="cat-dot" style={{ background: meta.color }} />
{meta.label}
</div>
<div className="kpi-number mt-1 text-lg font-bold">
{toPersianDigits(count)}
</div>
</div>
);
})}
</div>
</section>
{/* Critical items */}
{criticalItems.length > 0 && (
<section className="mb-7">
<SectionTitle>مواردی که نیازمند تصمیماند</SectionTitle>
<div className="overflow-x-auto rounded-lg border">
<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>
</TableRow>
</TableHeader>
<TableBody>
{criticalItems.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-semibold">{item.title}</TableCell>
<TableCell className="whitespace-nowrap text-muted-foreground">
{CATEGORY_META[item.category].label}
</TableCell>
<TableCell className="whitespace-nowrap text-muted-foreground">
{getHorizonLabelFa(item.horizon)}
</TableCell>
<TableCell className="tnum font-semibold">
{toPersianDigits(item.impactScore)}/۵
</TableCell>
<TableCell className="max-w-sm text-muted-foreground">
{item.whyItMatters}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</section>
)}
{/* Gap matrix */}
<section>
<SectionTitle>شکاف ویژگیهای {toPersianDigits(HORIZON_END)}</SectionTitle>
<div className="grid grid-cols-1 gap-2.5 md:grid-cols-2">
{byGap.map((dim) => (
<div key={dim.id} className="rounded-lg border p-3.5">
<div className="mb-1.5 flex items-center justify-between gap-2">
<span className="text-[12.5px] font-semibold">{dim.title}</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>
</div>
<div className="tnum flex items-center justify-between gap-2 text-[11px] text-muted-foreground">
<span>موجود {toPersianDigits(dim.currentScore)}/۱۰</span>
<span>هدف {toPersianDigits(dim.targetScore)}/۱۰</span>
<span>{toPersianDigits(dim.readinessPercentage)}٪ آمادگی</span>
</div>
{dim.strategicRecommendations[0] && (
<p className="mt-2 border-t pt-2 text-[11.5px] leading-relaxed text-muted-foreground">
<span className="font-semibold text-foreground">اقدام پیشنهادی: </span>
{dim.strategicRecommendations[0]}
</p>
)}
</div>
))}
</div>
</section>
</Card>
</div>
);
};
const SectionTitle: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<h3 className="mb-3 text-[14px] font-bold">{children}</h3>
);
const Figure: React.FC<{ label: string; value: string }> = ({ label, value }) => (
<div className="rounded-lg border p-3">
<div className="text-[11.5px] text-muted-foreground">{label}</div>
<div className="kpi-number mt-1 text-xl font-bold">{value}</div>
</div>
);