184 lines
7.4 KiB
TypeScript
184 lines
7.4 KiB
TypeScript
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>
|
||
);
|