feat: University Futures Radar — University of Isfahan foresight intelligence platform

This commit is contained in:
alireza 2026-08-27 09:05:45 +03:30
commit c6fbb9c7f2
53 changed files with 13867 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

8
.oxlintrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}

32
README.md Normal file
View File

@ -0,0 +1,32 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the Oxlint configuration
If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
```json
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"options": {
"typeAware": true
},
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
```
See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.

92
design_system.md Normal file
View File

@ -0,0 +1,92 @@
# نظام طراحی سامانه هوشمندی راهبردی دانشگاه اصفهان (Design System Specification)
این سند مرجع رسمی و استانداردهای طراحی دیداری، سلسله‌مراتب تایپوگرافی، پالت رنگ، الگوهای کامپوننت و راهنمای نگارش سامانه **«رادار آینده دانشگاه اصفهان»** است.
---
## ۱. تایپوگرافی (Typography Standard)
### ۱.۱. قلم واحد سازمانی (Universal Font Family)
- تمام متون، اعداد، نشان‌ها، دکمه‌ها و عناصر واسط کاربری باید منحصراً از قلم **Vazirmatn** استفاده کنند.
- استفاده از فونت‌های مونو یا سیستمی (`font-mono`, `Consolas`, `Courier`) برای اعداد و برچسب‌های فارسی **اکیداً ممنوع** است. اعداد فارسی باید با وزن متناسب قلم وزیرمتن نمایش داده شوند.
```css
*, ::before, ::after {
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', Tahoma, sans-serif !important;
}
```
### ۱.۲. مقیاس و سلسله‌مراتب تیترها (Heading Hierarchy & Contrast)
- **قاعده اساسی**: تیترها و عناوین باید به رنگ **مشکی کامل** (`#0f172a` یا `#000000`)، با وزن **بسیار ضخیم (Bold 800 - Black 900)** و با اندازه چشمگیر نسبت به متن بدنه باشند تا سلسله‌مراتب بصری در یک نگاه درک شود.
| سطح عنصر | نام کلاس | اندازه فونت (rem / px) | وزن قلم | رنگ متن | کاربرد |
|---|---|---|---|---|---|
| **Display / Hero** | `.text-display` | `2.25rem` (36px) | 900 (Black) | `#0f172a` | ارقام کلیدی داشبورد و شاخص‌های آمادگی ۱۴۱۵ |
| **Heading 1** | `.heading-1` / `h1` | `1.75rem` (28px) | 900 (Black) | `#0f172a` | عنوان اصلی هر صفحه |
| **Heading 2** | `.heading-2` / `h2` | `1.25rem` (20px) | 800 (ExtraBold) | `#0f172a` | تیتر بخش‌های اصلی و سرفصل‌ها |
| **Heading 3** | `.heading-3` / `h3` | `1.00rem` (16px) | 800 (ExtraBold) | `#0f172a` | عنوان کارت‌های رادار و پنل‌های تحلیلی |
| **Heading 4** | `.heading-4` / `h4` | `0.875rem` (14px) | 700 (Bold) | `#1e293b` | عناوین فرعی و آیتم‌های لیست |
| **Body Main** | `.body-main` / `p` | `0.8125rem` (13px) | 500 (Medium) | `#334155` | متن توضیحات و خلاصه‌های سیاستی |
| **Caption / Meta**| `.caption` | `0.75rem` (12px) | 600 (SemiBold) | `#64748b` | اطلاعات فراداده، افق زمانی و منابع |
| **Badge / Chip** | `.badge-label` | `0.6875rem` (11px) | 700 (Bold) | متناسب با دسته | برچسب‌های وضعیت و شمارنده‌ها |
---
## ۲. پالت رنگ و کنتراست (Color Palette)
### ۲.۱. بوم و پس‌زمینه‌ها (Canvas & Surfaces)
- **بوم اصلی (Canvas Background)**: `#f4f6fb` (خاکستری سرد و ملایم با مش گرادینت سبک)
- **کارت‌ها (Card Surface)**: `#ffffff` (سفید یکدست با سایه نرم `0 8px 24px -4px rgba(15,23,42,0.04)`)
- **کارت‌های فرعی (Subtle Surface)**: `#f8fafc`
- **خطوط حاشیه (Borders)**: `#e2e8f0` (خطوط تفکیک‌کننده محکم اما چشم‌نواز)
### ۲.۲. رنگ‌های موضوعی پاستلی (Pastel Theme Tints)
برای طبقه‌بندی هوشمندی آینده و ایجاد زیبایی در ساختار جعبه‌های بنتو:
- **روندها (Trends)**: پس‌زمینه `#eaf3fe` | حاشیه `#c8e0fd` | متن `#0284c7`
- **فناوری‌ها (Tech)**: پس‌زمینه `#e6f6f1` | حاشیه `#c4ecdf` | متن `#059669`
- **سیاست‌ها (Policies)**: پس‌زمینه `#fef7e6` | حاشیه `#fde8b8` | متن `#d97706`
- **سیگنال‌های آینده (Signals)**: پس‌زمینه `#fdecf2` | حاشیه `#f9cbd9` | متن `#e11d48`
- **برنامه‌ریزی ۱۴۱۵ / مثلث آینده**: پس‌زمینه `#f3effe` | حاشیه `#ded2fd` | متن `#7c3aed`
---
## ۳. نوار کناری ناوبری (Sidebar Standard)
- **بدنه نوار کناری**: پس‌زمینه سفید خالص (`#ffffff`)، مرز چپ با صفحه `#e2e8f0`، عرض ۲۷۰ پیکسل.
- **عناوین بخش‌ها**: خاکستری ملایم `#94a3b8`، فونت ۱۰.۵ پیکسل، وزن ۸۰۰، حروف کوچک، با فاصله مناسب از گروه قبلی.
- **دکمه‌های ناوبری**:
- *حالت عادی*: متن `#475569`، آیکون ۲۰ پیکسل با رنگ `#64748b`، گوشه‌های گرد ۱۲ پیکسل (`rounded-xl`).
- *حالت شناور (Hover)*: پس‌زمینه `#f1f5f9`، متن مشکی `#0f172a`.
- *حالت فعال (Active)*: پس‌زمینه سرمه‌ای تیره لوکس `#0f172a`، متن سفید خالص `#ffffff`، آیکون روشن، سایه ملایم.
- **شمارنده‌ها و برچسب‌ها**:
- نشان‌های عددی باید در سمت چپ آیتم قرار گیرند (`align-self: flex-start` در ساختار راست‌به‌چپ).
- متن شمارنده کاملاً با قلم وزیرمتن فارسی و بدون فونت مونو یا فاصله بی‌مورد درج شود.
- **حذف متون دوزبانه زاید**:
- تمامی پرانتزهای انگلیسی غیرضروری مانند `(Overview)`، `(Trends)`، `(Tech)`، `(Policies)`، `(Signals)`، `(Spider Gap)`، `(PULL/PUSH)` و `(CRUD)` در نوار کناری **ممنوع** بوده و با واژگان حرفه‌ای و فاخر فارسی جایگزین می‌شوند.
---
## ۴. استانداردهای هندسی نمودارها و مدل‌های بصری
### ۴.۱. مثلث هوشمندی آینده (Futures Triangle Geometry)
- **تناسب ابعادی استاندارد**:
- از ترسیم مثلث‌های عریض، پخ و تخت (نسبت عرض به ارتفاع ۲ به ۱) اکیداً پرهیز شود.
- نسبت ارتفاع به قاعده مثلث باید بین **۰.۸۵ تا ۰.۹۰** باشد (شبیه به مثلث متساوی‌الاضلاع کشیده و متوازن).
- اضلاع مثلث باید با خطوط رنگی ضخیم (حداقل ۴ پیکسل) و گرادینت‌های جهت‌دار متصل شوند.
- گره مرکزی (هوشمندی دانشگاه اصفهان) دقیقاً در گرانیگاه هندسی $(x_c, y_c)$ قرار گیرد.
### ۴.۲. رادار دایره‌ای آینده‌پژوهی (Futures Radar)
- دایره مرکزی سفید مرواریدی، پره‌های رنگی ملایم برای ۴ ربع، نقاط سیگنال با حاشیه سفید تمیز و عدد شدت اثر درون نقطه.
### ۴.۳. نمودار عنکبوتی ۱۰ بعدی (Spider Gap Chart)
- چندضلعی هدف دانشگاه اصفهان ۱۴۱۵ به رنگ سبز زمردی (`#00b894`) و چندضلعی وضع موجود به رنگ کهربایی (`#f59e0b`).
---
## ۵. راهنمای زبان و لحن سازمانی (Content & Tone Standards)
1. **حذف واژگان تکراری و عبارات توخالی (Zero AI-Slop)**:
- حذف برچسب‌های آکادمیک گل‌درشت مانند `نظریه سوهیل عنایت‌الله (Sohail Inayatullah)` از هدر صفحات و استفاده از برچسب‌های راهبردی خلاصه (`چارچوب آینده‌پژوهی`).
- حذف توضیحات شعاری و کش‌دار؛ محتوا باید مستقیماً پیامد دانشگاهی، عدد شاخص یا اقدام فوری را گزارش دهد.
2. **نگارش فارسی معیار و روان**:
- به کارگیری اصطلاحات دقیق آینده‌پژوهی و مدیریت دانشگاهی (مانند: *سیگنال‌های ضعیف، افق زمانی، شکاف بلوغ، شایستگی‌های نوظهور، آمایش آموزشی*).

17
index.html Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><circle cx='50' cy='50' r='46' fill='%23090e1a' stroke='%2300d4aa' stroke-width='4'/><circle cx='50' cy='50' r='30' fill='none' stroke='%2338bdf8' stroke-width='2' stroke-dasharray='4,4'/><circle cx='50' cy='50' r='14' fill='none' stroke='%2300d4aa' stroke-width='2'/><circle cx='50' cy='50' r='5' fill='%2300d4aa'/><line x1='50' y1='50' x2='82' y2='30' stroke='%2300d4aa' stroke-width='2.5'/></svg>" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="رادار آینده دانشگاه اصفهان — سامانه هوشمندی راهبردی، دیده‌بانی سیگنال‌ها، روندها، فناوری‌ها و تحلیل شکاف تا افق ۱۴۱۵" />
<title>رادار آینده دانشگاه اصفهان | University Futures Radar</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800;900&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body class="bg-navy">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

1305
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "uni",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "oxlint",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^1.34.0",
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@types/node": "^24.13.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.1.0",
"oxlint": "^1.79.0",
"typescript": "~6.0.2",
"vite": "^8.2.2"
}
}

1
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

24
public/icons.svg Normal file
View File

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

184
src/App.css Normal file
View File

@ -0,0 +1,184 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

274
src/App.tsx Normal file
View File

@ -0,0 +1,274 @@
import React, { useState, useEffect } from 'react';
import {
IntelligenceItem,
University1415Dimension,
JobEntity,
SkillEntity,
FuturesForce,
Category,
UserSession
} from './types/futures';
import { dataService } from './services/dataService';
import { authService } from './services/authService';
// Layout
import { Header } from './components/layout/Header';
import { Sidebar } from './components/layout/Sidebar';
// Modals
import { GlobalSearchModal } from './components/modals/GlobalSearchModal';
import { EntityEditorModal } from './components/modals/EntityEditorModal';
// Pages
import { Login } from './pages/Login';
import { Dashboard } from './pages/Dashboard';
import { RadarPage } from './pages/RadarPage';
import { ItemDetailPage } from './pages/ItemDetailPage';
import { LaborMarketPage } from './pages/LaborMarketPage';
import { University1415Page } from './pages/University1415Page';
import { GapAnalysisPage } from './pages/GapAnalysisPage';
import { FuturesTrianglePage } from './pages/FuturesTrianglePage';
import { StrategicNetworkPage } from './pages/StrategicNetworkPage';
import { ReportsPage } from './pages/ReportsPage';
import { AdminPage } from './pages/AdminPage';
import { SettingsPage } from './pages/SettingsPage';
export function App() {
const [currentSession, setCurrentSession] = useState<UserSession | null>(authService.getSession());
const [currentRoute, setCurrentRoute] = useState<string>('/');
const [selectedItem, setSelectedItem] = useState<IntelligenceItem | null>(null);
const [radarCategoryFilter, setRadarCategoryFilter] = useState<Category | 'all'>('all');
// Modals
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
// Live Data State
const [items, setItems] = useState<IntelligenceItem[]>([]);
const [dimensions, setDimensions] = useState<University1415Dimension[]>([]);
const [jobs, setJobs] = useState<JobEntity[]>([]);
const [skills, setSkills] = useState<SkillEntity[]>([]);
const [forces, setForces] = useState<FuturesForce[]>([]);
// Refresh all state from dataService
const refreshData = () => {
setItems(dataService.getAllItems());
setDimensions(dataService.getDimensions());
setJobs(dataService.getJobs());
setSkills(dataService.getSkills());
setForces(dataService.getFuturesForces());
};
useEffect(() => {
refreshData();
const unsub = authService.subscribe((sess) => {
setCurrentSession(sess);
if (!sess) {
setCurrentRoute('/login');
}
});
return () => unsub();
}, []);
// Global Ctrl+K listener
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
e.preventDefault();
setIsSearchOpen(true);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
const handleNavigate = (route: string) => {
setSelectedItem(null);
// Handle query params like /radar?category=trend
if (route.startsWith('/radar?category=')) {
const cat = route.split('=')[1] as Category;
setRadarCategoryFilter(cat);
setCurrentRoute('/radar');
} else {
setRadarCategoryFilter('all');
setCurrentRoute(route);
}
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleSelectItem = (item: IntelligenceItem) => {
setSelectedItem(item);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleSelectDimension = (dim: University1415Dimension) => {
setSelectedItem(null);
setCurrentRoute('/university-1415');
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleSaveNewItem = (newItem: IntelligenceItem) => {
dataService.createItem(newItem);
refreshData();
};
// If on login route or not authenticated
if (!currentSession || currentRoute === '/login') {
return (
<Login
onLoginSuccess={() => {
setCurrentRoute('/');
}}
/>
);
}
// Calculate section counts
const counts = {
signals: items.filter((i) => i.category === 'weak_signal').length,
trends: items.filter((i) => i.category === 'trend').length,
tech: items.filter((i) => i.category === 'technology').length,
policies: items.filter((i) => i.category === 'policy').length
};
return (
<div className="app-container">
{/* Institutional Sidebar */}
<Sidebar
currentRoute={selectedItem ? '' : currentRoute}
onNavigate={handleNavigate}
counts={counts}
/>
{/* Main Content Area */}
<div className="main-content">
<Header
currentSession={currentSession}
onOpenSearch={() => setIsSearchOpen(true)}
onNavigate={handleNavigate}
/>
<main className="page-wrapper">
{/* Detail Page View (Highest Priority) */}
{selectedItem ? (
<ItemDetailPage
item={selectedItem}
allItems={items}
dimensions={dimensions}
jobs={jobs}
skills={skills}
onBack={() => setSelectedItem(null)}
onSelectItem={handleSelectItem}
onSelectDimension={handleSelectDimension}
/>
) : (
<>
{currentRoute === '/' && (
<Dashboard
currentSession={currentSession}
items={items}
dimensions={dimensions}
onSelectItem={handleSelectItem}
onSelectDimension={handleSelectDimension}
onNavigate={handleNavigate}
/>
)}
{currentRoute === '/radar' && (
<RadarPage
items={items}
onSelectItem={handleSelectItem}
onOpenCreateModal={() => setIsCreateModalOpen(true)}
initialCategory={radarCategoryFilter}
/>
)}
{currentRoute === '/labor-market' && (
<LaborMarketPage jobs={jobs} skills={skills} />
)}
{currentRoute === '/university-1415' && (
<University1415Page
dimensions={dimensions}
items={items}
onSelectItem={handleSelectItem}
onRefreshDimensions={refreshData}
/>
)}
{currentRoute === '/gap-analysis' && (
<GapAnalysisPage
dimensions={dimensions}
onSelectDimension={handleSelectDimension}
/>
)}
{currentRoute === '/futures-triangle' && (
<FuturesTrianglePage forces={forces} />
)}
{currentRoute === '/network' && (
<StrategicNetworkPage
items={items}
dimensions={dimensions}
jobs={jobs}
skills={skills}
onSelectItem={handleSelectItem}
onSelectDimension={handleSelectDimension}
/>
)}
{currentRoute === '/reports' && (
<ReportsPage
items={items}
dimensions={dimensions}
jobs={jobs}
/>
)}
{currentRoute === '/admin' && (
<AdminPage
items={items}
dimensions={dimensions}
jobs={jobs}
skills={skills}
onRefreshData={refreshData}
onSelectItem={handleSelectItem}
/>
)}
{currentRoute === '/settings' && (
<SettingsPage onRefreshData={refreshData} />
)}
</>
)}
</main>
</div>
{/* Global Modals */}
<GlobalSearchModal
isOpen={isSearchOpen}
onClose={() => setIsSearchOpen(false)}
items={items}
dimensions={dimensions}
jobs={jobs}
skills={skills}
onSelectItem={handleSelectItem}
onSelectDimension={handleSelectDimension}
onSelectJob={() => {
setSelectedItem(null);
setCurrentRoute('/labor-market');
}}
/>
<EntityEditorModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSave={handleSaveNewItem}
/>
</div>
);
}
export default App;

BIN
src/assets/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

1
src/assets/react.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

1
src/assets/vite.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -0,0 +1,84 @@
import React from 'react';
import { University1415Dimension } from '../../types/futures';
import { toPersianDigits } from '../../utils/persianNumbers';
import { Target, ArrowLeft, Lightbulb, ArrowUpRight } from 'lucide-react';
interface Dimension1415CardProps {
dimension: University1415Dimension;
onClick: () => void;
}
export const Dimension1415Card: React.FC<Dimension1415CardProps> = ({ dimension, onClick }) => {
return (
<div
onClick={onClick}
className="intel-card interactive flex flex-col justify-between gap-3 text-right bg-white border border-slate-200 rounded-3xl p-5 shadow-sm hover:shadow-md"
style={{ borderTop: `4px solid ${dimension.pillarColor}` }}
>
<div>
{/* Header Badges */}
<div className="flex items-center justify-between gap-2 mb-2">
<span className="text-[10.5px] text-slate-400 font-bold">
{dimension.titleEn}
</span>
<span
className="text-[11px] font-bold px-2.5 py-0.5 rounded-full"
style={{
backgroundColor: `${dimension.pillarColor}15`,
color: dimension.pillarColor
}}
>
آمادگی: {toPersianDigits(dimension.readinessPercentage)}٪
</span>
</div>
{/* Title */}
<h4 className="text-slate-900 font-extrabold text-base mb-1.5 flex items-center justify-between">
<span>{dimension.title}</span>
<div className="circle-action-btn text-slate-700 w-7 h-7 text-xs">
<ArrowUpRight size={14} />
</div>
</h4>
{/* Definition */}
<p className="text-slate-600 text-xs leading-relaxed line-clamp-2">
{dimension.definition}
</p>
{/* Strategic Recommendation Preview */}
{dimension.strategicRecommendations[0] && (
<div className="mt-3 p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-[11px] text-slate-700 flex items-start gap-2">
<Lightbulb size={14} className="text-amber-500 shrink-0 mt-0.5" />
<span className="line-clamp-2">{dimension.strategicRecommendations[0]}</span>
</div>
)}
</div>
{/* Scores & Gap Meter */}
<div className="pt-3 border-t border-slate-100 flex flex-col gap-2">
<div className="flex items-center justify-between text-xs font-bold">
<span className="text-amber-600">
موجود: {toPersianDigits(dimension.currentScore)}
</span>
<span className="text-teal-600">
هدف: {toPersianDigits(dimension.targetScore)}
</span>
<span className="text-rose-600 bg-rose-50 px-2 py-0.5 rounded-full text-[11px]">
شکاف: {toPersianDigits(dimension.gap)}
</span>
</div>
{/* Readiness Bar */}
<div className="w-full h-2 bg-slate-100 rounded-full overflow-hidden flex">
<div
className="h-full rounded-full transition-all duration-500"
style={{
width: `${dimension.readinessPercentage}%`,
backgroundColor: dimension.pillarColor
}}
></div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,72 @@
import React from 'react';
import { IntelligenceItem } from '../../types/futures';
import {
toPersianDigits,
getCategoryLabelFa,
getCategoryColor,
getHorizonShortFa
} from '../../utils/persianNumbers';
import { Clock, Zap, ArrowUpRight } from 'lucide-react';
interface IntelligenceCardProps {
item: IntelligenceItem;
onClick: () => void;
}
export const IntelligenceCard: React.FC<IntelligenceCardProps> = ({ item, onClick }) => {
const catColor = getCategoryColor(item.category);
const isCritical = item.status === 'critical' || item.impactScore === 5;
return (
<div
onClick={onClick}
className={`intel-card interactive flex flex-col justify-between gap-3 text-right bg-white border border-slate-200 rounded-3xl p-5 shadow-sm hover:shadow-md ${
isCritical ? 'border-r-4 border-r-rose-500' : ''
}`}
>
<div>
{/* Top Badges */}
<div className="flex items-center justify-between gap-2 mb-2">
<span
className="text-[10px] font-bold px-2.5 py-0.5 rounded-full"
style={{
backgroundColor: `${catColor}15`,
color: catColor
}}
>
{getCategoryLabelFa(item.category)}
</span>
<div className="flex items-center gap-1 text-[11px] text-slate-400 font-medium">
<Clock size={12} />
<span>{getHorizonShortFa(item.horizon)}</span>
</div>
</div>
{/* Title */}
<h4 className="text-slate-900 font-extrabold text-sm leading-snug line-clamp-2 hover:text-teal-600 transition-colors">
{item.title}
</h4>
{/* Executive Summary */}
<p className="text-slate-600 text-xs leading-relaxed line-clamp-3 mt-2">
{item.executiveSummary}
</p>
</div>
{/* Footer Indicators */}
<div className="pt-3 border-t border-slate-100 flex items-center justify-between text-xs font-bold">
<div className="flex items-center gap-2">
<span className="text-teal-700 flex items-center gap-1">
<Zap size={13} className="text-teal-500" />
اثر: {toPersianDigits(item.impactScore)}/۵
</span>
</div>
<div className="circle-action-btn w-7 h-7 text-slate-600">
<ArrowUpRight size={14} />
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,110 @@
import React from 'react';
import { JobEntity, SkillEntity } from '../../types/futures';
import { toPersianDigits, getHorizonLabelFa } from '../../utils/persianNumbers';
import { Briefcase, Brain, TrendingUp, AlertTriangle, ShieldCheck } from 'lucide-react';
interface JobCardProps {
job: JobEntity;
}
export const JobCard: React.FC<JobCardProps> = ({ job }) => {
const isGrowing = job.type === 'growing';
return (
<div className="intel-card flex flex-col justify-between gap-3 text-right bg-white border border-slate-200 rounded-3xl p-5 shadow-sm hover:shadow-md">
<div>
<div className="flex items-center justify-between gap-2 mb-2">
<span className={`badge ${isGrowing ? 'badge-teal' : 'badge-rose'}`}>
{isGrowing ? (
<>
<TrendingUp size={12} />
شغل رو به رشد (+{toPersianDigits(job.growthRatePercent)}٪)
</>
) : (
<>
<AlertTriangle size={12} />
شغل در معرض تحول ({toPersianDigits(job.automationRiskPercent)}٪ ریسک)
</>
)}
</span>
<span className="text-[11px] text-slate-400 font-medium">{getHorizonLabelFa(job.horizon)}</span>
</div>
<h4 className="text-slate-900 font-extrabold text-sm mb-1">{job.title}</h4>
<div className="text-teal-700 text-xs font-bold mb-2">
دانشکده / گروه مرتبط: {job.discipline}
</div>
<p className="text-slate-600 text-xs leading-relaxed mb-3">
{job.description}
</p>
{/* Required Skills Badges */}
<div className="flex flex-wrap gap-1.5 mb-2">
{job.keyRequiredSkills.map((sk, idx) => (
<span
key={idx}
className="bg-slate-100 text-slate-700 border border-slate-200 text-[10.5px] px-2 py-0.5 rounded-full font-semibold"
>
{sk}
</span>
))}
</div>
</div>
{/* University Action Box */}
<div className="p-3 bg-slate-50 border border-slate-200 rounded-2xl text-xs flex items-start gap-2">
<ShieldCheck size={15} className="text-teal-600 shrink-0 mt-0.5" />
<div>
<span className="text-slate-500 text-[11px] block font-bold">اقدام دانشگاه اصفهان:</span>
<span className="text-slate-800 text-[11.5px] font-medium leading-relaxed">{job.universityAction}</span>
</div>
</div>
</div>
);
};
interface SkillCardProps {
skill: SkillEntity;
}
export const SkillCard: React.FC<SkillCardProps> = ({ skill }) => {
const isGrowing = skill.type === 'growing';
return (
<div className="intel-card flex flex-col justify-between gap-3 text-right bg-white border border-slate-200 rounded-3xl p-5 shadow-sm hover:shadow-md">
<div>
<div className="flex items-center justify-between gap-2 mb-2">
<span className={`badge ${isGrowing ? 'badge-purple' : 'badge-amber'}`}>
{isGrowing ? 'شایستگی صعودی' : 'مهارت در حال کاهش'}
</span>
<span className="text-slate-500 text-xs font-bold">
اهمیت: {toPersianDigits(skill.importanceScore)}/۵
</span>
</div>
<h4 className="text-slate-900 font-extrabold text-sm mb-2">{skill.title}</h4>
<p className="text-slate-600 text-xs leading-relaxed mb-3">
{skill.description}
</p>
{/* Affected Fields */}
<div className="text-xs text-slate-500 mb-2">
<span className="font-bold text-slate-700 block mb-1">رشتههای تحت تأثیر:</span>
<div className="flex flex-wrap gap-1">
{skill.affectedAcademicFields.map((f, idx) => (
<span key={idx} className="bg-slate-100 text-slate-700 text-[10.5px] px-2 py-0.5 rounded-full border border-slate-200">
{f}
</span>
))}
</div>
</div>
</div>
<div className="p-2.5 bg-slate-50 border border-slate-200 rounded-2xl text-[11px] text-slate-700">
<span className="text-teal-700 font-bold block mb-0.5">وضعیت جایگزینی / ارتقا:</span>
<span className="leading-relaxed">{skill.replacementOrAugmentation}</span>
</div>
</div>
);
};

View File

@ -0,0 +1,177 @@
import React, { useState } from 'react';
import { UserSession, PersonaType } from '../../types/futures';
import { authService, PRESET_USERS } from '../../services/authService';
import { Search, Bell, LogOut, Shield, Compass, ChevronDown, UserCheck, Sparkles } from 'lucide-react';
interface HeaderProps {
currentSession: UserSession | null;
onOpenSearch: () => void;
onNavigate: (route: string) => void;
}
export const Header: React.FC<HeaderProps> = ({ currentSession, onOpenSearch, onNavigate }) => {
const [showPersonaMenu, setShowPersonaMenu] = useState(false);
const [showNotifications, setShowNotifications] = useState(false);
const handleSwitchPersona = (role: PersonaType) => {
authService.switchPersona(role);
setShowPersonaMenu(false);
};
const handleLogout = () => {
authService.logout();
onNavigate('/login');
};
return (
<header className="no-print sticky top-0 z-40 w-full bg-white/90 backdrop-blur-md border-b border-slate-200 px-4 lg:px-8 py-3 flex items-center justify-between">
{/* Brand & Emblem */}
<div className="flex items-center gap-3.5 cursor-pointer" onClick={() => onNavigate('/')}>
<div className="w-10 h-10 rounded-2xl bg-gradient-to-br from-slate-900 to-slate-800 text-white flex items-center justify-center shadow-md shadow-slate-900/10">
<Compass size={22} className="text-teal-400" />
</div>
<div className="flex flex-col">
<div className="flex items-center gap-2">
<span className="text-slate-900 font-extrabold text-sm lg:text-base tracking-tight">
رادار آینده دانشگاه اصفهان
</span>
<span className="hidden sm:inline-block text-[10px] bg-teal-50 text-teal-700 font-bold px-2 py-0.5 rounded-full border border-teal-200">
افق ۱۴۱۵
</span>
</div>
<span className="text-slate-500 text-[11px] hidden md:block">
سامانه هوشمندی راهبردی و آیندهپژوهی آموزش عالی
</span>
</div>
</div>
{/* Center Global Search Trigger (Bento Pill Input) */}
<div className="flex-1 max-w-md mx-6 hidden md:block">
<button
onClick={onOpenSearch}
className="w-full bg-slate-100 hover:bg-slate-200/70 border border-slate-200 text-slate-500 hover:text-slate-800 px-4 py-2 rounded-full text-xs flex items-center justify-between transition-all shadow-sm"
>
<div className="flex items-center gap-2.5">
<Search size={15} className="text-slate-400" />
<span>جستجو در روندها، فناوریها، سیگنالها و مشاغل ۱۴۱۵...</span>
</div>
<kbd className="bg-white text-slate-600 text-[10px] px-2 py-0.5 rounded-full font-mono border border-slate-200 shadow-sm">
Ctrl + K
</kbd>
</button>
</div>
{/* Right Controls: Persona Switcher, Notifications, Profile */}
<div className="flex items-center gap-3">
{/* Mobile Search Button */}
<button
onClick={onOpenSearch}
className="md:hidden p-2 text-slate-600 hover:text-slate-900 bg-slate-100 border border-slate-200 rounded-xl"
title="جستجو"
>
<Search size={17} />
</button>
{/* Persona Perspective Switcher */}
<div className="relative">
<button
onClick={() => setShowPersonaMenu(!showPersonaMenu)}
className="flex items-center gap-2 bg-slate-100 hover:bg-slate-200/80 border border-slate-200 px-3.5 py-1.5 rounded-full text-xs font-bold text-slate-800 transition-colors shadow-sm"
>
<Shield size={14} className="text-teal-600" />
<span className="hidden sm:inline text-slate-500 font-medium">دیدگاه:</span>
<span className="text-teal-700">
{currentSession ? currentSession.roleTitleFa.split(' ')[0] : 'دیدگاه'}
</span>
<ChevronDown size={13} className="text-slate-400" />
</button>
{showPersonaMenu && (
<div className="absolute left-0 mt-2 w-64 bg-white border border-slate-200 rounded-2xl shadow-xl p-2 z-50 animate-fadeIn">
<div className="text-[11px] font-bold text-slate-400 px-3 py-1.5 border-b border-slate-100">
تغییر دیدگاه تحلیلی سامانه:
</div>
{(Object.keys(PRESET_USERS) as PersonaType[]).map((pKey) => {
const user = PRESET_USERS[pKey];
const isActive = currentSession?.role === pKey;
return (
<button
key={pKey}
onClick={() => handleSwitchPersona(pKey)}
className={`w-full text-right p-2.5 rounded-xl text-xs flex items-center justify-between transition-colors mt-1 ${
isActive
? 'bg-teal-50 text-teal-800 font-bold border border-teal-200'
: 'text-slate-700 hover:bg-slate-50'
}`}
>
<div>
<div className="font-bold">{user.roleTitleFa}</div>
<div className="text-[10.5px] text-slate-400">{user.fullName}</div>
</div>
{isActive && <UserCheck size={14} className="text-teal-600" />}
</button>
);
})}
</div>
)}
</div>
{/* Notifications Trigger */}
<div className="relative">
<button
onClick={() => setShowNotifications(!showNotifications)}
className="p-2 text-slate-600 hover:text-slate-900 bg-slate-100 hover:bg-slate-200/80 border border-slate-200 rounded-full transition-colors relative"
title="اعلان‌های رصدخانه"
>
<Bell size={17} />
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-rose-500 rounded-full"></span>
</button>
{showNotifications && (
<div className="absolute left-0 mt-2 w-80 bg-white border border-slate-200 rounded-2xl shadow-xl p-4 z-50 animate-fadeIn">
<div className="flex items-center justify-between pb-2 border-b border-slate-100">
<span className="text-xs font-extrabold text-slate-900">اعلانهای فوری رادار</span>
<span className="badge badge-rose">۳ سیگنال بحرانی</span>
</div>
<div className="flex flex-col gap-2.5 mt-2.5 text-xs">
<div className="p-2.5 bg-rose-50 border border-rose-200 rounded-xl">
<span className="font-bold text-rose-700 block">فراگیر شدن هوش مصنوعی زاینده</span>
<p className="text-slate-600 text-[11px] mt-0.5">ضرورت بازطراحی فرآیندهای سنجش آموزشی در نیمسال جاری</p>
</div>
<div className="p-2.5 bg-amber-50 border border-amber-200 rounded-xl">
<span className="font-bold text-amber-700 block">بحران آب و فرونشست اصفهان</span>
<p className="text-slate-600 text-[11px] mt-0.5">ابلاغ مأموریتهای استانی و بازنگری پژوهانهها</p>
</div>
</div>
</div>
)}
</div>
{/* User Profile Pill & Logout */}
<div className="flex items-center gap-2 pl-1 border-r border-slate-200 pr-3">
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-full bg-gradient-to-tr from-teal-500 to-sky-500 text-white font-black text-xs flex items-center justify-center shadow-sm">
{currentSession?.fullName ? currentSession.fullName.slice(0, 1) : 'د'}
</div>
<div className="hidden xl:flex flex-col text-right">
<span className="text-xs font-extrabold text-slate-900 leading-tight">
{currentSession?.fullName || 'کاربر سازمانی'}
</span>
<span className="text-[10.5px] text-slate-500">
{currentSession?.roleTitleFa || 'ریاست دانشگاه'}
</span>
</div>
</div>
<button
onClick={handleLogout}
className="p-2 text-slate-400 hover:text-rose-600 hover:bg-rose-50 rounded-full transition-colors"
title="خروج از سامانه"
>
<LogOut size={16} />
</button>
</div>
</div>
</header>
);
};

View File

@ -0,0 +1,109 @@
import React from 'react';
import { UserSession, PersonaType } from '../../types/futures';
import { authService } from '../../services/authService';
import { Shield, Sparkles, Target, Compass } from 'lucide-react';
interface PersonaBannerProps {
currentSession: UserSession | null;
}
export const PersonaBanner: React.FC<PersonaBannerProps> = ({ currentSession }) => {
const currentRole = currentSession?.role || 'president';
const personaConfig: Record<
PersonaType,
{ title: string; question: string; icon: React.ReactNode; bg: string; border: string; accent: string; badge: string }
> = {
president: {
title: 'دیدگاه ریاست و هیئت رئیسه دانشگاه',
question: '«این تغییرات، تهدیدها و فرصت‌ها برای کلیت دانشگاه اصفهان چه معنایی دارد و اولویت فوری چیست؟»',
icon: <Shield size={16} />,
bg: 'bg-emerald-50/70',
border: 'border-emerald-200',
accent: 'text-emerald-700',
badge: 'bg-emerald-100 text-emerald-800'
},
researcher: {
title: 'دیدگاه پژوهشگر آینده‌پژوهی و تحلیل‌گر علم',
question: '«چه سیگنال‌های ضعیف، فناوری‌های اخلال‌گر و روندهای جدیدی در حال ظهور در آموزش عالی هستند؟»',
icon: <Compass size={16} />,
bg: 'bg-sky-50/70',
border: 'border-sky-200',
accent: 'text-sky-700',
badge: 'bg-sky-100 text-sky-800'
},
planner: {
title: 'دیدگاه برنامه‌ریز راهبردی و تحول آموزشی',
question: '«شکاف میان وضعیت موجود و چشم‌انداز دانشگاه اصفهان ۱۴۱۵ کجاست و چه برنامه‌هایی باید اولویت یابند؟»',
icon: <Target size={16} />,
bg: 'bg-amber-50/70',
border: 'border-amber-200',
accent: 'text-amber-700',
badge: 'bg-amber-100 text-amber-800'
},
admin: {
title: 'دیدگاه راهبری و مدیریت سیستم هوشمندی',
question: '«مدیریت یکپارچه پایگاه داده، ارزیابی شاخص‌ها، ثبت سیگنال‌های تازه و صدور گزارش‌های نهادی»',
icon: <Sparkles size={16} />,
bg: 'bg-purple-50/70',
border: 'border-purple-200',
accent: 'text-purple-700',
badge: 'bg-purple-100 text-purple-800'
}
};
const currentConfig = personaConfig[currentRole];
return (
<div
className={`w-full ${currentConfig.bg} border ${currentConfig.border} rounded-2xl px-5 py-3 mb-2 flex flex-col md:flex-row items-start md:items-center justify-between gap-3 text-xs shadow-sm`}
>
<div className="flex items-center gap-3">
<span className={`p-2 rounded-xl bg-white ${currentConfig.accent} border ${currentConfig.border} shadow-sm`}>
{currentConfig.icon}
</span>
<div>
<span className="font-extrabold text-slate-900 ml-2">{currentConfig.title}:</span>
<span className="text-slate-600 font-medium italic">{currentConfig.question}</span>
</div>
</div>
{/* Quick Perspective Switching Pills */}
<div className="flex items-center gap-1.5 self-end md:self-auto shrink-0 text-[11px]">
<span className="text-slate-400 hidden lg:inline font-semibold">تغییر لنز:</span>
<div className="flex items-center bg-white border border-slate-200 p-1 rounded-full shadow-sm gap-1">
<button
onClick={() => authService.switchPersona('president')}
className={`px-3 py-1 rounded-full font-bold transition-all ${
currentRole === 'president'
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
ریاست
</button>
<button
onClick={() => authService.switchPersona('researcher')}
className={`px-3 py-1 rounded-full font-bold transition-all ${
currentRole === 'researcher'
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
پژوهشگر
</button>
<button
onClick={() => authService.switchPersona('planner')}
className={`px-3 py-1 rounded-full font-bold transition-all ${
currentRole === 'planner'
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
برنامهریز
</button>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,304 @@
import React, { useState } from 'react';
import {
LayoutDashboard,
Radar,
Briefcase,
Target,
GitPullRequest,
Triangle,
GitFork,
FileText,
Database,
Settings,
ChevronDown,
ChevronLeft,
Sparkles,
TrendingUp,
Cpu,
BookOpen
} from 'lucide-react';
import { toPersianDigits } from '../../utils/persianNumbers';
interface SidebarProps {
currentRoute: string;
onNavigate: (route: string) => void;
counts: {
signals: number;
trends: number;
tech: number;
policies: number;
};
}
export const Sidebar: React.FC<SidebarProps> = ({ currentRoute, onNavigate, counts }) => {
const [radarExpanded, setRadarExpanded] = useState(true);
const isActive = (route: string) => currentRoute === route;
return (
<aside className="no-print w-64 lg:w-72 bg-white border-l border-slate-200 flex flex-col justify-between p-4 select-none shrink-0 z-30 shadow-[4px_0_24px_-4px_rgba(15,23,42,0.03)]">
<div className="flex flex-col gap-1.5 overflow-y-auto">
{/* Navigation Group 1: Foresight Observation */}
<div className="text-[11px] font-extrabold text-slate-400 px-3 pt-2 pb-1">
دیدهبانی و رصد هوشمند
</div>
{/* Dashboard */}
<button
onClick={() => onNavigate('/')}
className={`w-full flex items-center justify-between px-3.5 py-2.5 rounded-xl text-xs font-bold transition-all ${
isActive('/')
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-700 hover:bg-slate-100 hover:text-slate-900'
}`}
>
<div className="flex items-center gap-3">
<LayoutDashboard size={18} className={isActive('/') ? 'text-teal-400' : 'text-slate-400'} />
<span className="font-bold">داشبورد اصلی</span>
</div>
</button>
{/* Radar & Sub-sections */}
<div>
<button
onClick={() => {
onNavigate('/radar');
setRadarExpanded(!radarExpanded);
}}
className={`w-full flex items-center justify-between px-3.5 py-2.5 rounded-xl text-xs font-bold transition-all ${
isActive('/radar')
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-700 hover:bg-slate-100 hover:text-slate-900'
}`}
>
<div className="flex items-center gap-3">
<Radar size={18} className={isActive('/radar') ? 'text-teal-400' : 'text-slate-400'} />
<span className="font-bold">رادار آینده</span>
</div>
<div className="flex items-center gap-2">
<span className={`text-[11px] px-2 py-0.5 rounded-full font-bold leading-none ${
isActive('/radar') ? 'bg-slate-800 text-teal-300' : 'bg-slate-100 text-slate-600'
}`}>
{toPersianDigits(counts.signals + counts.trends + counts.tech + counts.policies)}
</span>
{radarExpanded ? <ChevronDown size={14} className="opacity-60" /> : <ChevronLeft size={14} className="opacity-60" />}
</div>
</button>
{radarExpanded && (
<div className="mr-5 pr-2 border-r-2 border-slate-100 flex flex-col gap-1 mt-1">
<button
onClick={() => onNavigate('/radar?category=trend')}
className="flex items-center justify-between py-1.5 px-3 rounded-lg text-xs font-semibold text-slate-600 hover:text-slate-900 hover:bg-sky-50 transition-colors"
>
<span className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-sky-500"></span>
<span>روندها</span>
</span>
<span className="text-[11px] text-sky-600 font-bold">
{toPersianDigits(counts.trends)}
</span>
</button>
<button
onClick={() => onNavigate('/radar?category=technology')}
className="flex items-center justify-between py-1.5 px-3 rounded-lg text-xs font-semibold text-slate-600 hover:text-slate-900 hover:bg-teal-50 transition-colors"
>
<span className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-teal-500"></span>
<span>فناوریها</span>
</span>
<span className="text-[11px] text-teal-600 font-bold">
{toPersianDigits(counts.tech)}
</span>
</button>
<button
onClick={() => onNavigate('/radar?category=policy')}
className="flex items-center justify-between py-1.5 px-3 rounded-lg text-xs font-semibold text-slate-600 hover:text-slate-900 hover:bg-amber-50 transition-colors"
>
<span className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-amber-500"></span>
<span>سیاستها</span>
</span>
<span className="text-[11px] text-amber-700 font-bold">
{toPersianDigits(counts.policies)}
</span>
</button>
<button
onClick={() => onNavigate('/radar?category=weak_signal')}
className="flex items-center justify-between py-1.5 px-3 rounded-lg text-xs font-semibold text-slate-600 hover:text-slate-900 hover:bg-rose-50 transition-colors"
>
<span className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-rose-500"></span>
<span>سیگنالهای ضعیف</span>
</span>
<span className="text-[11px] text-rose-600 font-bold">
{toPersianDigits(counts.signals)}
</span>
</button>
</div>
)}
</div>
{/* Future Labor Market */}
<button
onClick={() => onNavigate('/labor-market')}
className={`w-full flex items-center justify-between px-3.5 py-2.5 rounded-xl text-xs font-bold transition-all ${
isActive('/labor-market')
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-700 hover:bg-slate-100 hover:text-slate-900'
}`}
>
<div className="flex items-center gap-3">
<Briefcase size={18} className={isActive('/labor-market') ? 'text-amber-400' : 'text-slate-400'} />
<span className="font-bold">بازار کار آینده</span>
</div>
</button>
{/* Navigation Group 2: Strategic Planning */}
<div className="text-[11px] font-extrabold text-slate-400 px-3 pt-4 pb-1">
برنامهریزی راهبردی ۱۴۱۵
</div>
{/* University 1415 */}
<button
onClick={() => onNavigate('/university-1415')}
className={`w-full flex items-center justify-between px-3.5 py-2.5 rounded-xl text-xs font-bold transition-all ${
isActive('/university-1415')
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-700 hover:bg-slate-100 hover:text-slate-900'
}`}
>
<div className="flex items-center gap-3">
<Target size={18} className={isActive('/university-1415') ? 'text-teal-400' : 'text-slate-400'} />
<span className="font-bold">دانشگاه اصفهان ۱۴۱۵</span>
</div>
<span className={`text-[10px] px-2 py-0.5 rounded-full font-bold ${
isActive('/university-1415') ? 'bg-slate-800 text-teal-300' : 'bg-teal-50 text-teal-700'
}`}>
۱۰ بعد
</span>
</button>
{/* Gap Analysis */}
<button
onClick={() => onNavigate('/gap-analysis')}
className={`w-full flex items-center justify-between px-3.5 py-2.5 rounded-xl text-xs font-bold transition-all ${
isActive('/gap-analysis')
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-700 hover:bg-slate-100 hover:text-slate-900'
}`}
>
<div className="flex items-center gap-3">
<GitPullRequest size={18} className={isActive('/gap-analysis') ? 'text-rose-400' : 'text-slate-400'} />
<span className="font-bold">تحلیل شکاف</span>
</div>
<span className={`text-[10px] px-2 py-0.5 rounded-full font-bold ${
isActive('/gap-analysis') ? 'bg-slate-800 text-rose-300' : 'bg-rose-50 text-rose-700'
}`}>
وضع موجود
</span>
</button>
{/* Futures Triangle */}
<button
onClick={() => onNavigate('/futures-triangle')}
className={`w-full flex items-center justify-between px-3.5 py-2.5 rounded-xl text-xs font-bold transition-all ${
isActive('/futures-triangle')
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-700 hover:bg-slate-100 hover:text-slate-900'
}`}
>
<div className="flex items-center gap-3">
<Triangle size={18} className={isActive('/futures-triangle') ? 'text-purple-400' : 'text-slate-400'} />
<span className="font-bold">مثلث آینده</span>
</div>
</button>
{/* Strategic Impact Network */}
<button
onClick={() => onNavigate('/network')}
className={`w-full flex items-center justify-between px-3.5 py-2.5 rounded-xl text-xs font-bold transition-all ${
isActive('/network')
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-700 hover:bg-slate-100 hover:text-slate-900'
}`}
>
<div className="flex items-center gap-3">
<GitFork size={18} className={isActive('/network') ? 'text-teal-400' : 'text-slate-400'} />
<span className="font-bold">شبکه روابط راهبردی</span>
</div>
</button>
{/* Reports & Briefs */}
<button
onClick={() => onNavigate('/reports')}
className={`w-full flex items-center justify-between px-3.5 py-2.5 rounded-xl text-xs font-bold transition-all ${
isActive('/reports')
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-700 hover:bg-slate-100 hover:text-slate-900'
}`}
>
<div className="flex items-center gap-3">
<FileText size={18} className={isActive('/reports') ? 'text-sky-400' : 'text-slate-400'} />
<span className="font-bold">گزارشها و تحلیلها</span>
</div>
<span className={`text-[10px] px-2 py-0.5 rounded-full font-bold ${
isActive('/reports') ? 'bg-slate-800 text-sky-300' : 'bg-sky-50 text-sky-700'
}`}>
نسخه چاپی
</span>
</button>
{/* Navigation Group 3: Configuration */}
<div className="text-[11px] font-extrabold text-slate-400 px-3 pt-4 pb-1">
پیکربندی سامانه
</div>
{/* Admin CRUD */}
<button
onClick={() => onNavigate('/admin')}
className={`w-full flex items-center justify-between px-3.5 py-2.5 rounded-xl text-xs font-bold transition-all ${
isActive('/admin')
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-700 hover:bg-slate-100 hover:text-slate-900'
}`}
>
<div className="flex items-center gap-3">
<Database size={18} className={isActive('/admin') ? 'text-teal-400' : 'text-slate-400'} />
<span className="font-bold">مدیریت دادهها</span>
</div>
</button>
{/* Settings */}
<button
onClick={() => onNavigate('/settings')}
className={`w-full flex items-center justify-between px-3.5 py-2.5 rounded-xl text-xs font-bold transition-all ${
isActive('/settings')
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-700 hover:bg-slate-100 hover:text-slate-900'
}`}
>
<div className="flex items-center gap-3">
<Settings size={18} className={isActive('/settings') ? 'text-slate-400' : 'text-slate-400'} />
<span className="font-bold">تنظیمات</span>
</div>
</button>
</div>
{/* Institutional Branding Footer Card */}
<div className="pt-3 border-t border-slate-100">
<div className="p-3 bg-slate-50 border border-slate-200/80 rounded-2xl flex flex-col gap-0.5 text-right">
<div className="flex items-center justify-between text-xs font-black text-slate-900">
<span>دانشگاه اصفهان</span>
<span className="w-2 h-2 rounded-full bg-teal-500"></span>
</div>
<span className="text-[11px] text-slate-500 font-semibold">سامانه هوشمندی راهبردی</span>
<span className="text-[10px] text-slate-400 font-bold mt-0.5">افق دهساله ۱۴۱۵</span>
</div>
</div>
</aside>
);
};

View File

@ -0,0 +1,258 @@
import React, { useState } from 'react';
import { IntelligenceItem, Category, Horizon, ItemStatus } from '../../types/futures';
import { X, Save } from 'lucide-react';
interface EntityEditorModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (item: IntelligenceItem) => void;
initialItem?: IntelligenceItem | null;
}
export const EntityEditorModal: React.FC<EntityEditorModalProps> = ({
isOpen,
onClose,
onSave,
initialItem
}) => {
const isEditing = !!initialItem;
const [title, setTitle] = useState(initialItem?.title || '');
const [category, setCategory] = useState<Category>(initialItem?.category || 'trend');
const [horizon, setHorizon] = useState<Horizon>(initialItem?.horizon || 'near');
const [status, setStatus] = useState<ItemStatus>(initialItem?.status || 'active');
const [impactScore, setImpactScore] = useState<number>(initialItem?.impactScore || 4);
const [uncertaintyScore, setUncertaintyScore] = useState<number>(initialItem?.uncertaintyScore || 2);
const [geographicScope, setGeographicScope] = useState<'ایران' | 'منطقه‌ای' | 'جهانی'>(
initialItem?.geographicScope || 'ایران'
);
const [summary, setSummary] = useState(initialItem?.executiveSummary || '');
const [whyItMatters, setWhyItMatters] = useState(initialItem?.whyItMatters || '');
const [relevance, setRelevance] = useState(initialItem?.relevanceToUniversity || '');
if (!isOpen) return null;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim() || !summary.trim()) return;
const item: IntelligenceItem = {
id: initialItem?.id || `custom-${Date.now()}`,
title,
category,
horizon,
status,
impactScore,
uncertaintyScore,
confidence: 'high',
geographicScope,
executiveSummary: summary,
whyItMatters,
relevanceToUniversity: relevance,
impact: initialItem?.impact || {
education: impactScore,
research: Math.max(1, impactScore - 1),
operations: 3,
students: impactScore,
faculty: Math.max(1, impactScore - 1),
laborMarket: 4,
strategicImportance: impactScore
},
evidence: initialItem?.evidence || [
{
title: 'شواهد ثبت شده توسط کاربر در سامانه هوشمندی',
source: 'داده‌ورزی داخلی دانشگاه اصفهان',
date: '۱۴۰۳/۰۶/۰۱',
snippet: summary
}
],
implications: initialItem?.implications || {
opportunities: ['فرصت نوآوری آموزشی و پاسخ به تقاضای بازار'],
risks: ['خطر عدم انطباق بهنگام برنامه‌های درسی'],
threats: ['عقب‌ماندگی از رقبای منطقه‌ای'],
requiredCapabilities: ['توسعه زیرساخت‌ها و بازآموزی اساتید'],
affectedDepartments: ['معاونت آموزشی', 'گروه‌های آموزشی مرتبط']
},
relatedItemIds: initialItem?.relatedItemIds || [],
relatedJobIds: initialItem?.relatedJobIds || [],
relatedSkillIds: initialItem?.relatedSkillIds || [],
relatedDimensionIds: initialItem?.relatedDimensionIds || ['dim-1']
};
onSave(item);
onClose();
};
return (
<div className="modal-overlay" onClick={onClose}>
<div
className="modal-content p-6 flex flex-col gap-4 text-right animate-fadeIn max-w-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
<h3 className="text-white font-black text-base">
{isEditing ? 'ویرایش موجودیت هوشمندی' : 'افزودن سیگنال / روند / فناوری جدید'}
</h3>
<button onClick={onClose} className="p-1 text-slate-400 hover:text-white rounded-lg">
<X size={18} />
</button>
</div>
<form onSubmit={handleSubmit} className="flex flex-col gap-4 text-xs">
{/* Title */}
<div className="flex flex-col gap-1">
<label className="text-slate-300 font-semibold">عنوان موجودیت:</label>
<input
type="text"
required
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="مثال: دوقلوی دیجیتال آزمایشگاه‌های مهندسی دانشگاه اصفهان"
className="bg-[#091122] border border-slate-800 rounded-lg p-2.5 text-white outline-none focus:border-teal-500 text-xs"
/>
</div>
{/* Row: Category, Horizon, Status */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="flex flex-col gap-1">
<label className="text-slate-300 font-semibold">دستهبندی رادار:</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value as Category)}
className="bg-[#091122] border border-slate-800 rounded-lg p-2.5 text-white outline-none focus:border-teal-500 text-xs"
>
<option value="trend">روند (Trend)</option>
<option value="technology">فناوری (Technology)</option>
<option value="policy">سیاست و حکمرانی (Policy)</option>
<option value="weak_signal">سیگنال ضعیف آینده (Weak Signal)</option>
</select>
</div>
<div className="flex flex-col gap-1">
<label className="text-slate-300 font-semibold">افق زمانی تحقق:</label>
<select
value={horizon}
onChange={(e) => setHorizon(e.target.value as Horizon)}
className="bg-[#091122] border border-slate-800 rounded-lg p-2.5 text-white outline-none focus:border-teal-500 text-xs"
>
<option value="now">اکنون (۱۴۰۳-۱۴۰۵)</option>
<option value="near">نزدیک (۱۴۰۵-۱۴۰۸)</option>
<option value="mid">میانمدت (۱۴۰۸-۱۴۱۱)</option>
<option value="long">بلندمدت (۱۴۱۱-۱۴۱۵)</option>
</select>
</div>
<div className="flex flex-col gap-1">
<label className="text-slate-300 font-semibold">وضعیت رصد:</label>
<select
value={status}
onChange={(e) => setStatus(e.target.value as ItemStatus)}
className="bg-[#091122] border border-slate-800 rounded-lg p-2.5 text-white outline-none focus:border-teal-500 text-xs"
>
<option value="active">فعال (Active)</option>
<option value="emerging">نوظهور (Emerging)</option>
<option value="monitoring">تحت پایش (Monitoring)</option>
<option value="critical">بحرانی / فوری (Critical)</option>
</select>
</div>
</div>
{/* Row: Impact Score & Scope */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="flex flex-col gap-1">
<label className="text-slate-300 font-semibold">ضریب تأثیر (۱ تا ۵):</label>
<input
type="number"
min={1}
max={5}
value={impactScore}
onChange={(e) => setImpactScore(Number(e.target.value))}
className="bg-[#091122] border border-slate-800 rounded-lg p-2 text-white outline-none focus:border-teal-500 text-xs"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-slate-300 font-semibold">عدم قطعیت (۱ تا ۵):</label>
<input
type="number"
min={1}
max={5}
value={uncertaintyScore}
onChange={(e) => setUncertaintyScore(Number(e.target.value))}
className="bg-[#091122] border border-slate-800 rounded-lg p-2 text-white outline-none focus:border-teal-500 text-xs"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-slate-300 font-semibold">دامنه جغرافیایی:</label>
<select
value={geographicScope}
onChange={(e) => setGeographicScope(e.target.value as any)}
className="bg-[#091122] border border-slate-800 rounded-lg p-2.5 text-white outline-none focus:border-teal-500 text-xs"
>
<option value="ایران">ایران</option>
<option value="منطقه‌ای">منطقهای (خاورمیانه / استان اصفهان)</option>
<option value="جهانی">جهانی</option>
</select>
</div>
</div>
{/* Summary */}
<div className="flex flex-col gap-1">
<label className="text-slate-300 font-semibold">خلاصه مدیریتی (Executive Summary):</label>
<textarea
rows={2}
required
value={summary}
onChange={(e) => setSummary(e.target.value)}
placeholder="توضیح کوتاه در ۲ الی ۳ جمله درباره این پدیده..."
className="bg-[#091122] border border-slate-800 rounded-lg p-2.5 text-white outline-none focus:border-teal-500 text-xs leading-relaxed"
/>
</div>
{/* Why It Matters */}
<div className="flex flex-col gap-1">
<label className="text-slate-300 font-semibold">چرا برای آموزش عالی اهمیت دارد؟ (Why it matters):</label>
<textarea
rows={2}
value={whyItMatters}
onChange={(e) => setWhyItMatters(e.target.value)}
placeholder="چرا دانشگاه باید به این موضوع واکنش نشان دهد..."
className="bg-[#091122] border border-slate-800 rounded-lg p-2.5 text-white outline-none focus:border-teal-500 text-xs leading-relaxed"
/>
</div>
{/* Relevance to University */}
<div className="flex flex-col gap-1">
<label className="text-slate-300 font-semibold">ارتباط مستقیم با دانشگاه اصفهان:</label>
<input
type="text"
value={relevance}
onChange={(e) => setRelevance(e.target.value)}
placeholder="مثال: تأثیر بر گروه‌های آموزشی مهندسی و علوم پایه دانشگاه اصفهان"
className="bg-[#091122] border border-slate-800 rounded-lg p-2.5 text-white outline-none focus:border-teal-500 text-xs"
/>
</div>
{/* Actions */}
<div className="flex items-center justify-end gap-3 pt-3 border-t border-slate-800">
<button
type="button"
onClick={onClose}
className="btn btn-secondary text-xs py-2 px-4"
>
انصراف
</button>
<button
type="submit"
className="btn btn-primary text-xs py-2 px-5 flex items-center gap-2"
>
<Save size={15} />
<span>{isEditing ? 'ذخیره تغییرات' : 'ثبت در رادار هوشمندی'}</span>
</button>
</div>
</form>
</div>
</div>
);
};

View File

@ -0,0 +1,86 @@
import React, { useState } from 'react';
import { X, CheckCircle, Mail } from 'lucide-react';
interface ForgotPasswordModalProps {
isOpen: boolean;
onClose: () => void;
}
export const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ isOpen, onClose }) => {
const [identifier, setIdentifier] = useState('');
const [isSubmitted, setIsSubmitted] = useState(false);
if (!isOpen) return null;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!identifier.trim()) return;
setIsSubmitted(true);
};
return (
<div className="modal-overlay" onClick={onClose}>
<div
className="modal-content p-6 flex flex-col gap-4 text-right max-w-md animate-fadeIn"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
<h3 className="text-white font-black text-sm">بازیابی گذرواژه دانشگاهی</h3>
<button onClick={onClose} className="p-1 text-slate-400 hover:text-white rounded-lg">
<X size={18} />
</button>
</div>
{isSubmitted ? (
<div className="flex flex-col items-center text-center py-6 gap-3">
<CheckCircle size={40} className="text-teal-400" />
<h4 className="text-white font-bold text-sm">پیامک / ایمیل بازیابی ارسال شد</h4>
<p className="text-slate-300 text-xs leading-relaxed max-w-xs">
لینک اختصاصی تعیین رمز عبور جدید به شناسه رسمی شما در پورتال دانشگاه اصفهان ارسال گردید.
</p>
<button onClick={onClose} className="btn btn-primary text-xs py-2 px-6 mt-2">
بازگشت به صفحه ورود
</button>
</div>
) : (
<form onSubmit={handleSubmit} className="flex flex-col gap-4 text-xs">
<p className="text-slate-300 leading-relaxed">
لطفاً پست الکترونیکی دانشگاهی (با پسوند ui.ac.ir@) یا شماره تلفن همراه متصل به کارتابل اداری خود را وارد نمایید.
</p>
<div className="flex flex-col gap-1.5">
<label className="text-slate-400 font-semibold">پست الکترونیکی یا شماره همراه:</label>
<div className="relative">
<input
type="text"
required
value={identifier}
onChange={(e) => setIdentifier(e.target.value)}
placeholder="name@ui.ac.ir یا 0913xxxxxxx"
className="w-full bg-[#091122] border border-slate-800 rounded-lg p-2.5 pl-8 text-white outline-none focus:border-teal-500 text-xs"
/>
<Mail size={15} className="absolute left-2.5 top-3 text-slate-500" />
</div>
</div>
<div className="flex items-center justify-end gap-2 pt-3 border-t border-slate-800">
<button
type="button"
onClick={onClose}
className="btn btn-secondary text-xs py-2 px-4"
>
انصراف
</button>
<button
type="submit"
className="btn btn-primary text-xs py-2 px-5"
>
ارسال لینک بازیابی
</button>
</div>
</form>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,289 @@
import React, { useState, useMemo, useEffect } from 'react';
import { IntelligenceItem, University1415Dimension, JobEntity, SkillEntity } from '../../types/futures';
import {
toPersianDigits,
getCategoryLabelFa,
getCategoryColor,
getHorizonLabelFa
} from '../../utils/persianNumbers';
import { Search, X, Clock, Zap, ArrowLeft, Target, Briefcase, Brain } from 'lucide-react';
interface GlobalSearchModalProps {
isOpen: boolean;
onClose: () => void;
items: IntelligenceItem[];
dimensions: University1415Dimension[];
jobs: JobEntity[];
skills: SkillEntity[];
onSelectItem: (item: IntelligenceItem) => void;
onSelectDimension: (dim: University1415Dimension) => void;
onSelectJob: () => void;
}
export const GlobalSearchModal: React.FC<GlobalSearchModalProps> = ({
isOpen,
onClose,
items,
dimensions,
jobs,
skills,
onSelectItem,
onSelectDimension,
onSelectJob
}) => {
const [query, setQuery] = useState('');
const [activeFilter, setActiveFilter] = useState<'all' | 'items' | 'dimensions' | 'jobs' | 'skills'>('all');
// Keyboard shortcut listener for Esc
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
const results = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) {
// Return top priority items if query is empty
return {
matchedItems: items.slice(0, 5),
matchedDimensions: dimensions.slice(0, 3),
matchedJobs: jobs.slice(0, 3),
matchedSkills: skills.slice(0, 3)
};
}
const matchedItems = items.filter(
(i) =>
i.title.toLowerCase().includes(q) ||
i.executiveSummary.toLowerCase().includes(q) ||
i.whyItMatters.toLowerCase().includes(q)
);
const matchedDimensions = dimensions.filter(
(d) =>
d.title.toLowerCase().includes(q) ||
d.definition.toLowerCase().includes(q) ||
d.vision.toLowerCase().includes(q)
);
const matchedJobs = jobs.filter(
(j) =>
j.title.toLowerCase().includes(q) ||
j.description.toLowerCase().includes(q) ||
j.discipline.toLowerCase().includes(q)
);
const matchedSkills = skills.filter(
(s) =>
s.title.toLowerCase().includes(q) ||
s.description.toLowerCase().includes(q)
);
return { matchedItems, matchedDimensions, matchedJobs, matchedSkills };
}, [query, items, dimensions, jobs, skills]);
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div
className="modal-content p-5 flex flex-col gap-4 text-right animate-fadeIn"
onClick={(e) => e.stopPropagation()}
>
{/* Search Input Bar */}
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
<div className="flex items-center gap-3 flex-1">
<Search size={20} className="text-teal-400" />
<input
type="text"
autoFocus
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="جستجو در تمام موجودیت‌های رادار، ابعاد ۱۴۱۵، مشاغل و مهارت‌ها..."
className="bg-transparent border-none outline-none text-white text-sm w-full placeholder:text-slate-500"
/>
</div>
<button onClick={onClose} className="p-1 text-slate-400 hover:text-white rounded-lg">
<X size={18} />
</button>
</div>
{/* Category Filters */}
<div className="flex flex-wrap items-center gap-2 text-xs">
<button
onClick={() => setActiveFilter('all')}
className={`px-3 py-1 rounded-full transition-all ${
activeFilter === 'all' ? 'bg-teal-500 text-slate-950 font-bold' : 'bg-slate-800 text-slate-300'
}`}
>
همه نتایج
</button>
<button
onClick={() => setActiveFilter('items')}
className={`px-3 py-1 rounded-full transition-all ${
activeFilter === 'items' ? 'bg-teal-500 text-slate-950 font-bold' : 'bg-slate-800 text-slate-300'
}`}
>
روندها و فناوریها ({toPersianDigits(results.matchedItems.length)})
</button>
<button
onClick={() => setActiveFilter('dimensions')}
className={`px-3 py-1 rounded-full transition-all ${
activeFilter === 'dimensions' ? 'bg-teal-500 text-slate-950 font-bold' : 'bg-slate-800 text-slate-300'
}`}
>
ابعاد ۱۴۱۵ ({toPersianDigits(results.matchedDimensions.length)})
</button>
<button
onClick={() => setActiveFilter('jobs')}
className={`px-3 py-1 rounded-full transition-all ${
activeFilter === 'jobs' ? 'bg-teal-500 text-slate-950 font-bold' : 'bg-slate-800 text-slate-300'
}`}
>
مشاغل ({toPersianDigits(results.matchedJobs.length)})
</button>
<button
onClick={() => setActiveFilter('skills')}
className={`px-3 py-1 rounded-full transition-all ${
activeFilter === 'skills' ? 'bg-teal-500 text-slate-950 font-bold' : 'bg-slate-800 text-slate-300'
}`}
>
مهارتها ({toPersianDigits(results.matchedSkills.length)})
</button>
</div>
{/* Results List */}
<div className="flex flex-col gap-3 max-h-[60vh] overflow-y-auto pr-1">
{/* Intelligence Items (Trends, Tech, Policies, Signals) */}
{(activeFilter === 'all' || activeFilter === 'items') && results.matchedItems.length > 0 && (
<div className="flex flex-col gap-2">
<div className="text-[11px] font-bold text-slate-400 px-1">روندها، فناوریها و سیگنالها:</div>
{results.matchedItems.map((item) => (
<div
key={item.id}
onClick={() => {
onSelectItem(item);
onClose();
}}
className="bg-[#0b1428] hover:bg-[#11203d] border border-slate-800 hover:border-teal-500/40 p-3 rounded-xl cursor-pointer transition-all flex flex-col gap-1"
>
<div className="flex items-center justify-between gap-2">
<span
className="text-[10px] font-bold px-2 py-0.5 rounded-full"
style={{
backgroundColor: `${getCategoryColor(item.category)}22`,
color: getCategoryColor(item.category)
}}
>
{getCategoryLabelFa(item.category)}
</span>
<span className="text-[10.5px] text-slate-400 flex items-center gap-1">
<Clock size={11} />
{getHorizonLabelFa(item.horizon)}
</span>
</div>
<h5 className="text-white text-xs font-bold mt-1">{item.title}</h5>
<p className="text-slate-400 text-[11px] line-clamp-2">{item.executiveSummary}</p>
</div>
))}
</div>
)}
{/* 1415 Dimensions */}
{(activeFilter === 'all' || activeFilter === 'dimensions') && results.matchedDimensions.length > 0 && (
<div className="flex flex-col gap-2 mt-2">
<div className="text-[11px] font-bold text-teal-400 px-1 flex items-center gap-1">
<Target size={13} />
ابعاد دهگانه دانشگاه اصفهان ۱۴۱۵:
</div>
{results.matchedDimensions.map((dim) => (
<div
key={dim.id}
onClick={() => {
onSelectDimension(dim);
onClose();
}}
className="bg-[#0b1428] hover:bg-[#11203d] border border-teal-500/30 p-3 rounded-xl cursor-pointer transition-all flex items-center justify-between gap-3"
>
<div>
<h5 className="text-white text-xs font-bold">{dim.title}</h5>
<p className="text-slate-400 text-[11px] line-clamp-1 mt-0.5">{dim.definition}</p>
</div>
<div className="text-left shrink-0 text-xs">
<span className="text-rose-400 font-bold">شکاف: {toPersianDigits(dim.gap)}</span>
<div className="text-[10px] text-teal-400 font-semibold">{toPersianDigits(dim.readinessPercentage)}٪ آمادگی</div>
</div>
</div>
))}
</div>
)}
{/* Jobs */}
{(activeFilter === 'all' || activeFilter === 'jobs') && results.matchedJobs.length > 0 && (
<div className="flex flex-col gap-2 mt-2">
<div className="text-[11px] font-bold text-amber-400 px-1 flex items-center gap-1">
<Briefcase size={13} />
مشاغل آینده آموزش عالی:
</div>
{results.matchedJobs.map((job) => (
<div
key={job.id}
onClick={() => {
onSelectJob();
onClose();
}}
className="bg-[#0b1428] hover:bg-[#11203d] border border-slate-800 p-2.5 rounded-xl cursor-pointer transition-all flex items-center justify-between text-xs"
>
<div>
<span className="text-white font-bold">{job.title}</span>
<span className="text-slate-400 text-[11px] block">{job.discipline}</span>
</div>
<span className={`badge ${job.type === 'growing' ? 'badge-teal' : 'badge-rose'}`}>
{job.type === 'growing' ? `+${toPersianDigits(job.growthRatePercent)}٪ رشد` : `${toPersianDigits(job.automationRiskPercent)}٪ ریسک اتوماسیون`}
</span>
</div>
))}
</div>
)}
{/* Skills */}
{(activeFilter === 'all' || activeFilter === 'skills') && results.matchedSkills.length > 0 && (
<div className="flex flex-col gap-2 mt-2">
<div className="text-[11px] font-bold text-purple-400 px-1 flex items-center gap-1">
<Brain size={13} />
شایستگیها و مهارتهای آینده:
</div>
{results.matchedSkills.map((skill) => (
<div
key={skill.id}
onClick={() => {
onSelectJob();
onClose();
}}
className="bg-[#0b1428] hover:bg-[#11203d] border border-slate-800 p-2.5 rounded-xl cursor-pointer transition-all flex items-center justify-between text-xs"
>
<span className="text-slate-200">{skill.title}</span>
<span className="text-slate-500 text-[11px]">رده: {skill.category}</span>
</div>
))}
</div>
)}
{results.matchedItems.length === 0 &&
results.matchedDimensions.length === 0 &&
results.matchedJobs.length === 0 && (
<div className="text-center py-8 text-slate-500 text-xs">
موردی منطبق با «{query}» در پایگاه هوشمندی یافت نشد.
</div>
)}
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,376 @@
import React, { useState, useMemo } from 'react';
import { IntelligenceItem, Category, Horizon } from '../../types/futures';
import {
calculateItemRadarCoords,
getHorizonRadiusRatio
} from '../../utils/radarMath';
import {
toPersianDigits,
getCategoryLabelFa,
getHorizonLabelFa,
getCategoryColor
} from '../../utils/persianNumbers';
import { Eye, Clock, Zap } from 'lucide-react';
interface FuturesRadarProps {
items: IntelligenceItem[];
selectedCategory?: Category | 'all';
selectedHorizon?: Horizon | 'all';
onSelectItem: (item: IntelligenceItem) => void;
height?: number;
}
export const FuturesRadar: React.FC<FuturesRadarProps> = ({
items,
selectedCategory = 'all',
selectedHorizon = 'all',
onSelectItem,
height = 640
}) => {
const [hoveredItem, setHoveredItem] = useState<{
item: IntelligenceItem;
x: number;
y: number;
} | null>(null);
const size = 720;
const center = size / 2;
const maxRadius = size / 2 - 36;
// Filter items according to props
const filteredItems = useMemo(() => {
return items.filter((item) => {
if (selectedCategory !== 'all' && item.category !== selectedCategory) return false;
if (selectedHorizon !== 'all' && item.horizon !== selectedHorizon) return false;
return true;
});
}, [items, selectedCategory, selectedHorizon]);
// Group items by category to calculate non-overlapping angles
const itemsWithCoords = useMemo(() => {
const groups: Record<Category, IntelligenceItem[]> = {
trend: [],
technology: [],
policy: [],
weak_signal: []
};
filteredItems.forEach((item) => {
groups[item.category].push(item);
});
const result: { item: IntelligenceItem; x: number; y: number; radius: number }[] = [];
(Object.keys(groups) as Category[]).forEach((cat) => {
const list = groups[cat];
list.forEach((item, idx) => {
const coords = calculateItemRadarCoords(
item.id,
item.category,
item.horizon,
idx,
list.length,
center,
maxRadius
);
result.push({
item,
x: coords.x,
y: coords.y,
radius: coords.radius
});
});
});
return result;
}, [filteredItems, center, maxRadius]);
const rings: { horizon: Horizon; label: string; ratio: number }[] = [
{ horizon: 'now', label: 'اکنون (۱۴۰۳-۱۴۰۵)', ratio: getHorizonRadiusRatio('now') },
{ horizon: 'near', label: 'نزدیک (۱۴۰۵-۱۴۰۸)', ratio: getHorizonRadiusRatio('near') },
{ horizon: 'mid', label: 'میان‌مدت (۱۴۰۸-۱۴۱۱)', ratio: getHorizonRadiusRatio('mid') },
{ horizon: 'long', label: 'بلندمدت (۱۴۱۱-۱۴۱۵)', ratio: getHorizonRadiusRatio('long') }
];
return (
<div className="relative w-full flex flex-col items-center justify-center select-none" style={{ minHeight: height }}>
<svg
viewBox={`0 0 ${size} ${size}`}
className="w-full max-w-[680px] h-auto drop-shadow-md"
style={{ overflow: 'visible' }}
>
<defs>
{/* Radial radar background gradient for Neo-Clean Bento canvas */}
<radialGradient id="radarBgGrad" cx="50%" cy="50%" r="50%">
<stop offset="0%" stopColor="#ffffff" stopOpacity="1" />
<stop offset="70%" stopColor="#fbfcfe" stopOpacity="1" />
<stop offset="100%" stopColor="#f3f6fa" stopOpacity="1" />
</radialGradient>
{/* Sweeping radar scan gradient */}
<linearGradient id="scanGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#00b894" stopOpacity="0.25" />
<stop offset="60%" stopColor="#00b894" stopOpacity="0.04" />
<stop offset="100%" stopColor="#00b894" stopOpacity="0" />
</linearGradient>
{/* Glow Filters for high-impact dots */}
<filter id="radarShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" floodColor="#0f172a" floodOpacity="0.12" />
</filter>
</defs>
{/* Outer Circular Bounds */}
<circle
cx={center}
cy={center}
r={maxRadius}
fill="url(#radarBgGrad)"
stroke="#e2e8f0"
strokeWidth="1.5"
filter="url(#radarShadow)"
/>
{/* Quadrant Sector Pastel Tint Washes */}
{/* Top-Right: Trends (Soft Sky) */}
<path
d={`M ${center} ${center} L ${center + maxRadius} ${center} A ${maxRadius} ${maxRadius} 0 0 0 ${center} ${center - maxRadius} Z`}
fill="rgba(2, 132, 199, 0.035)"
/>
{/* Top-Left: Technologies (Soft Mint) */}
<path
d={`M ${center} ${center} L ${center} ${center - maxRadius} A ${maxRadius} ${maxRadius} 0 0 0 ${center - maxRadius} ${center} Z`}
fill="rgba(0, 184, 148, 0.035)"
/>
{/* Bottom-Left: Weak Signals (Soft Coral/Rose) */}
<path
d={`M ${center} ${center} L ${center - maxRadius} ${center} A ${maxRadius} ${maxRadius} 0 0 0 ${center} ${center + maxRadius} Z`}
fill="rgba(244, 63, 94, 0.035)"
/>
{/* Bottom-Right: Policies (Soft Amber) */}
<path
d={`M ${center} ${center} L ${center} ${center + maxRadius} A ${maxRadius} ${maxRadius} 0 0 0 ${center + maxRadius} ${center} Z`}
fill="rgba(245, 158, 11, 0.035)"
/>
{/* Sweeping Radar Scan Line */}
<g className="radar-sweep-line pointer-events-none">
<path
d={`M ${center} ${center} L ${center + maxRadius} ${center} A ${maxRadius} ${maxRadius} 0 0 0 ${center} ${center - maxRadius} Z`}
fill="url(#scanGradient)"
/>
<line
x1={center}
y1={center}
x2={center + maxRadius}
y2={center}
stroke="#00b894"
strokeWidth="1.5"
strokeOpacity="0.75"
/>
</g>
{/* Concentric Horizon Rings */}
{rings.map((ring, idx) => {
const r = maxRadius * ring.ratio;
const isHighlighted = selectedHorizon === ring.horizon;
return (
<g key={ring.horizon}>
<circle
cx={center}
cy={center}
r={r}
fill="none"
stroke={isHighlighted ? '#00b894' : '#e2e8f0'}
strokeWidth={isHighlighted ? '2' : '1'}
strokeDasharray={idx === 0 ? 'none' : '4,4'}
opacity={isHighlighted ? 1 : 0.85}
/>
{/* Ring Label (Horizon text on vertical axis) */}
<text
x={center}
y={center - r + 13}
fill="#64748b"
fontSize="9.5"
fontWeight="600"
textAnchor="middle"
className="pointer-events-none"
>
{ring.label}
</text>
</g>
);
})}
{/* Quadrant Divider Axes */}
<line
x1={center - maxRadius}
y1={center}
x2={center + maxRadius}
y2={center}
stroke="#cbd5e1"
strokeWidth="1.2"
strokeDasharray="4,4"
/>
<line
x1={center}
y1={center - maxRadius}
x2={center}
y2={center + maxRadius}
stroke="#cbd5e1"
strokeWidth="1.2"
strokeDasharray="4,4"
/>
{/* Center Institutional Hub */}
<circle cx={center} cy={center} r={20} fill="#ffffff" stroke="#00b894" strokeWidth="2.5" filter="url(#radarShadow)" />
<circle cx={center} cy={center} r={7} fill="#00b894" className="anim-pulse" />
<text
x={center}
y={center + 32}
fill="#1e293b"
fontSize="9.5"
fontWeight="800"
textAnchor="middle"
className="pointer-events-none"
>
دانشگاه اصفهان
</text>
{/* Quadrant Corner Badges (Modern Bento Pills) */}
{/* Top-Right: Trends */}
<g transform={`translate(${center + maxRadius * 0.48}, ${center - maxRadius * 0.78})`}>
<rect x="-65" y="-13" width="130" height="26" rx="13" fill="#ffffff" stroke="#bae6fd" strokeWidth="1.5" filter="url(#radarShadow)" />
<text textAnchor="middle" y="4.5" fill="#0284c7" fontSize="11" fontWeight="800">روندها (Trends)</text>
</g>
{/* Top-Left: Technologies */}
<g transform={`translate(${center - maxRadius * 0.48}, ${center - maxRadius * 0.78})`}>
<rect x="-70" y="-13" width="140" height="26" rx="13" fill="#ffffff" stroke="#a7f3d0" strokeWidth="1.5" filter="url(#radarShadow)" />
<text textAnchor="middle" y="4.5" fill="#059669" fontSize="11" fontWeight="800">فناوریها (Tech)</text>
</g>
{/* Bottom-Right: Policies */}
<g transform={`translate(${center + maxRadius * 0.48}, ${center + maxRadius * 0.82})`}>
<rect x="-65" y="-13" width="130" height="26" rx="13" fill="#ffffff" stroke="#fde68a" strokeWidth="1.5" filter="url(#radarShadow)" />
<text textAnchor="middle" y="4.5" fill="#d97706" fontSize="11" fontWeight="800">سیاستها (Policies)</text>
</g>
{/* Bottom-Left: Weak Signals */}
<g transform={`translate(${center - maxRadius * 0.48}, ${center + maxRadius * 0.82})`}>
<rect x="-75" y="-13" width="150" height="26" rx="13" fill="#ffffff" stroke="#fecdd3" strokeWidth="1.5" filter="url(#radarShadow)" />
<text textAnchor="middle" y="4.5" fill="#e11d48" fontSize="11" fontWeight="800">سیگنالهای آینده (Signals)</text>
</g>
{/* Radar Item Blips */}
{itemsWithCoords.map(({ item, x, y }) => {
const color = getCategoryColor(item.category);
// Dot radius based on impactScore (1 to 5)
const dotR = 6 + item.impactScore * 2;
const isCritical = item.status === 'critical' || item.impactScore === 5;
const isHovered = hoveredItem?.item.id === item.id;
return (
<g
key={item.id}
transform={`translate(${x}, ${y})`}
className="cursor-pointer transition-transform duration-150"
style={{ transformBox: 'fill-box' }}
onClick={() => onSelectItem(item)}
onMouseEnter={() => setHoveredItem({ item, x, y })}
onMouseLeave={() => setHoveredItem(null)}
>
{/* Pulsing outer aura for critical signals */}
{isCritical && (
<circle
r={dotR + 6}
fill="none"
stroke={color}
strokeWidth="1.5"
opacity="0.4"
className="anim-pulse"
/>
)}
{/* Hover Highlight Ring */}
{isHovered && (
<circle
r={dotR + 7}
fill="none"
stroke="#0f172a"
strokeWidth="2"
opacity="0.7"
/>
)}
{/* Main Core Dot (Vibrant color with crisp white border) */}
<circle
r={dotR}
fill={color}
stroke="#ffffff"
strokeWidth="2.5"
filter="url(#radarShadow)"
/>
{/* Impact Number */}
<text
y="3.5"
textAnchor="middle"
fill="#ffffff"
fontSize={dotR > 10 ? '9.5' : '8.5'}
fontWeight="900"
className="pointer-events-none"
>
{toPersianDigits(item.impactScore)}
</text>
</g>
);
})}
</svg>
{/* Floating Hover Tooltip Card (Bento Light Style) */}
{hoveredItem && (
<div
className="absolute pointer-events-none z-50 bg-white border border-slate-200 p-4 rounded-2xl shadow-xl max-w-xs text-right animate-fadeIn"
style={{
top: Math.max(10, hoveredItem.y - 120),
left: Math.max(10, Math.min(size - 220, hoveredItem.x - 90))
}}
>
<div className="flex items-center justify-between gap-2 mb-1.5">
<span
className="text-[10px] font-bold px-2 py-0.5 rounded-full"
style={{
backgroundColor: `${getCategoryColor(hoveredItem.item.category)}18`,
color: getCategoryColor(hoveredItem.item.category)
}}
>
{getCategoryLabelFa(hoveredItem.item.category)}
</span>
<span className="text-[10px] font-semibold text-slate-400">
{getHorizonLabelFa(hoveredItem.item.horizon)}
</span>
</div>
<h5 className="text-slate-900 font-extrabold text-xs leading-snug mb-1">
{hoveredItem.item.title}
</h5>
<p className="text-slate-500 text-[11px] line-clamp-2 leading-relaxed mb-2">
{hoveredItem.item.whyItMatters}
</p>
<div className="flex items-center justify-between text-[10px] pt-1.5 border-t border-slate-100 font-bold">
<span className="text-teal-600 flex items-center gap-1">
<Zap size={11} />
شدت اثر: {toPersianDigits(hoveredItem.item.impactScore)} از ۵
</span>
<span className="text-slate-400 flex items-center gap-0.5">
کلیک جهت مشاهده پرونده
</span>
</div>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,134 @@
import React from 'react';
import { ImpactAssessment } from '../../types/futures';
import { toPersianDigits } from '../../utils/persianNumbers';
import {
GraduationCap,
Microscope,
Cpu,
Users,
Briefcase,
Target,
Sparkles
} from 'lucide-react';
interface ImpactGaugesProps {
impact: ImpactAssessment;
}
interface DimensionConfig {
key: keyof ImpactAssessment;
title: string;
desc: string;
icon: React.ReactNode;
color: string;
}
export const ImpactGauges: React.FC<ImpactGaugesProps> = ({ impact }) => {
const dimensions: DimensionConfig[] = [
{
key: 'education',
title: 'آموزش و برنامه‌های درسی',
desc: 'میزان دگرگونی در شیوه‌های تدریس، یادگیری و امتحانات',
icon: <GraduationCap size={16} />,
color: '#38bdf8'
},
{
key: 'research',
title: 'پژوهش و تولید علم',
desc: 'تأثیر بر سرعت تحقیقات، روش‌شناسی و آزمایشگاه‌ها',
icon: <Microscope size={16} />,
color: '#a855f7'
},
{
key: 'operations',
title: 'عملیات و فرآیندهای اداری',
desc: 'تغییر در خدمات دانشجویی، مالی و مدیریت پردیس',
icon: <Cpu size={16} />,
color: '#00d4aa'
},
{
key: 'students',
title: 'تجربه و آینده دانشجویان',
desc: 'اثر بر مهارت‌ها، انگیزه، سلامت روان و هویت دانشجو',
icon: <Users size={16} />,
color: '#fbbf24'
},
{
key: 'faculty',
title: 'اعضای هیئت علمی',
desc: 'تحول در نقش استاد، بار کاری و معیارهای ارتقا',
icon: <Sparkles size={16} />,
color: '#ec4899'
},
{
key: 'laborMarket',
title: 'بازار کار و اشتغال‌پذیری',
desc: 'تغییر در تقاضای کارفرمایان و مهارت‌های شغلی موردنیاز',
icon: <Briefcase size={16} />,
color: '#f97316'
},
{
key: 'strategicImportance',
title: 'اهمیت راهبردی برای دانشگاه اصفهان',
desc: 'وزن در تصمیم‌گیری هیئت رئیسه و اولویت‌بندی بودجه',
icon: <Target size={16} />,
color: '#f43f5e'
}
];
return (
<div className="flex flex-col gap-3">
{dimensions.map((dim) => {
const score = impact[dim.key] || 1;
const percent = (score / 5) * 100;
return (
<div
key={dim.key}
className="bg-[#0b1429] border border-slate-800/80 rounded-xl p-3.5 flex flex-col gap-2 hover:border-slate-700 transition-colors"
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 text-xs font-bold text-slate-200">
<span
className="p-1.5 rounded-lg flex items-center justify-center"
style={{ backgroundColor: `${dim.color}1a`, color: dim.color }}
>
{dim.icon}
</span>
<span>{dim.title}</span>
</div>
<div className="flex items-center gap-1 text-xs">
<span className="font-extrabold text-white text-sm">
{toPersianDigits(score)}
</span>
<span className="text-slate-500 text-[11px]">از ۵</span>
</div>
</div>
<p className="text-[11px] text-slate-400">{dim.desc}</p>
{/* Score Bar */}
<div className="w-full h-2 bg-slate-800/90 rounded-full overflow-hidden flex">
<div
className="h-full rounded-full transition-all duration-500"
style={{
width: `${percent}%`,
backgroundColor: dim.color
}}
></div>
</div>
{/* 5 Dots Indicator */}
<div className="flex items-center justify-between px-1 text-[9px] text-slate-500">
<span>خیلی کم (۱)</span>
<span>کم (۲)</span>
<span>متوسط (۳)</span>
<span>زیاد (۴)</span>
<span>بسیار زیاد (۵)</span>
</div>
</div>
);
})}
</div>
);
};

View File

@ -0,0 +1,160 @@
import React, { useState } from 'react';
import { JobEntity, SkillEntity } from '../../types/futures';
import { toPersianDigits } from '../../utils/persianNumbers';
import { TrendingUp, AlertTriangle, Brain, Sparkles, Code, Users } from 'lucide-react';
interface LaborChartsProps {
jobs: JobEntity[];
skills: SkillEntity[];
}
export const LaborCharts: React.FC<LaborChartsProps> = ({ jobs, skills }) => {
const [activeTab, setActiveTab] = useState<'jobs' | 'skills'>('jobs');
const growingJobs = jobs.filter((j) => j.type === 'growing').slice(0, 8);
const transformingJobs = jobs.filter((j) => j.type === 'transforming').slice(0, 8);
const skillCategories = [
{ key: 'cognitive', title: 'شناختی و تحلیلی', icon: <Brain size={15} />, color: '#8b5cf6', count: 10 },
{ key: 'digital', title: 'دیجیتال و هوش مصنوعی', icon: <Code size={15} />, color: '#00b894', count: 12 },
{ key: 'social', title: 'اجتماعی و انسانی', icon: <Users size={15} />, color: '#f59e0b', count: 8 },
{ key: 'technical', title: 'فنی و مهندسی', icon: <Sparkles size={15} />, color: '#0284c7', count: 6 }
];
return (
<div className="w-full flex flex-col gap-5 animate-fadeIn">
{/* Tab Switcher (Pill Style) */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-slate-200 pb-3">
<div className="flex items-center bg-slate-100 p-1 rounded-full text-xs shadow-sm">
<button
onClick={() => setActiveTab('jobs')}
className={`py-1.5 px-4 rounded-full font-bold flex items-center gap-1.5 transition-all ${
activeTab === 'jobs'
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
<TrendingUp size={14} />
نقشه رشد و ریسک اتوماسیون مشاغل
</button>
<button
onClick={() => setActiveTab('skills')}
className={`py-1.5 px-4 rounded-full font-bold flex items-center gap-1.5 transition-all ${
activeTab === 'skills'
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
<Brain size={14} />
توزیع شایستگیهای نوظهور و افول
</button>
</div>
<span className="text-slate-400 text-xs font-semibold hidden md:inline">
تحلیل انطباق با زیستبوم صنعتی و فناوری استان اصفهان
</span>
</div>
{activeTab === 'jobs' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5 animate-fadeIn">
{/* Top Growing Jobs Ranking */}
<div className="intel-card bg-white border border-slate-200 rounded-3xl p-5 shadow-sm flex flex-col gap-3">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<h4 className="text-teal-700 font-extrabold text-sm flex items-center gap-2">
<TrendingUp size={16} className="text-teal-600" />
بیشترین نرخ رشد تقاضای شغلی (افق تا ۱۴۱۵)
</h4>
<span className="badge badge-teal">رشد تصاعدی</span>
</div>
<div className="flex flex-col gap-2.5 mt-1">
{growingJobs.map((j) => (
<div key={j.id} className="bg-slate-50 p-3 rounded-2xl border border-slate-200 text-xs">
<div className="flex items-center justify-between mb-1">
<span className="font-extrabold text-slate-900">{j.title}</span>
<span className="font-bold text-teal-700 font-mono text-[13px]">
+{toPersianDigits(j.growthRatePercent)}٪
</span>
</div>
<div className="flex items-center justify-between text-[11px] text-slate-500 font-medium">
<span>رشته مرتبط: {j.discipline}</span>
<span className="text-slate-400">
ریسک اتوماسیون: {toPersianDigits(j.automationRiskPercent)}٪
</span>
</div>
<div className="w-full h-1.5 bg-slate-200 rounded-full mt-2 overflow-hidden">
<div
className="h-full bg-teal-500 rounded-full"
style={{ width: `${Math.min(100, j.growthRatePercent * 1.8)}%` }}
></div>
</div>
</div>
))}
</div>
</div>
{/* Top Transforming Jobs & Automation Vulnerability */}
<div className="intel-card bg-white border border-slate-200 rounded-3xl p-5 shadow-sm flex flex-col gap-3">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<h4 className="text-rose-700 font-extrabold text-sm flex items-center gap-2">
<AlertTriangle size={16} className="text-rose-600" />
بیشترین ریسک اتوماسیون و نیاز به بازآفرینی
</h4>
<span className="badge badge-rose">در معرض تغییر</span>
</div>
<div className="flex flex-col gap-2.5 mt-1">
{transformingJobs.map((j) => (
<div key={j.id} className="bg-slate-50 p-3 rounded-2xl border border-slate-200 text-xs">
<div className="flex items-center justify-between mb-1">
<span className="font-extrabold text-slate-900">{j.title}</span>
<span className="font-bold text-rose-700 font-mono text-[13px]">
{toPersianDigits(j.automationRiskPercent)}٪ خطر اتوماسیون
</span>
</div>
<div className="flex items-center justify-between text-[11px] text-slate-500 font-medium">
<span>حوزه: {j.discipline}</span>
<span className="text-amber-700 font-bold">نیاز مبرم به بازآموزی</span>
</div>
<div className="w-full h-1.5 bg-slate-200 rounded-full mt-2 overflow-hidden">
<div
className="h-full bg-rose-500 rounded-full"
style={{ width: `${j.automationRiskPercent}%` }}
></div>
</div>
</div>
))}
</div>
</div>
</div>
)}
{activeTab === 'skills' && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 animate-fadeIn">
{skillCategories.map((cat) => (
<div key={cat.key} className="intel-card bg-white border border-slate-200 rounded-3xl p-5 shadow-sm flex flex-col justify-between gap-3">
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-slate-900">{cat.title}</span>
<span
className="p-2 rounded-xl"
style={{ backgroundColor: `${cat.color}15`, color: cat.color }}
>
{cat.icon}
</span>
</div>
<div className="my-1">
<span className="kpi-number text-3xl font-black text-slate-900">
{toPersianDigits(cat.count)}
</span>
<span className="text-xs text-slate-400 mr-1 font-bold">مهارت کلیدی</span>
</div>
<span className="text-[11px] text-slate-500 border-t border-slate-100 pt-2 font-medium">
تأکید در کوریکولوم دانشگاه اصفهان
</span>
</div>
))}
</div>
)}
</div>
);
};

View File

@ -0,0 +1,241 @@
import React, { useState } from 'react';
import { University1415Dimension } from '../../types/futures';
import { getSpiderCoordinates, pointsToSvgPath } from '../../utils/radarMath';
import { toPersianDigits } from '../../utils/persianNumbers';
interface SpiderChartProps {
dimensions: University1415Dimension[];
onSelectDimension?: (dim: University1415Dimension) => void;
height?: number;
}
export const SpiderChart: React.FC<SpiderChartProps> = ({
dimensions,
onSelectDimension,
height = 580
}) => {
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
const size = 640;
const center = size / 2;
const maxRadius = size / 2 - 70;
const numAxes = dimensions.length; // 10
const currentScores = dimensions.map((d) => d.currentScore);
const targetScores = dimensions.map((d) => d.targetScore);
const currentPoints = getSpiderCoordinates(currentScores, 10, center, maxRadius);
const targetPoints = getSpiderCoordinates(targetScores, 10, center, maxRadius);
const currentPath = pointsToSvgPath(currentPoints);
const targetPath = pointsToSvgPath(targetPoints);
// Concentric polygon grids (2, 4, 6, 8, 10)
const gridLevels = [2, 4, 6, 8, 10];
return (
<div className="relative w-full flex flex-col items-center justify-center select-none" style={{ minHeight: height }}>
<svg
viewBox={`0 0 ${size} ${size}`}
className="w-full max-w-[620px] h-auto drop-shadow-2xl"
style={{ overflow: 'visible' }}
>
<defs>
{/* Gradients */}
<linearGradient id="spiderTargetGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#00d4aa" stopOpacity="0.38" />
<stop offset="100%" stopColor="#0284c7" stopOpacity="0.22" />
</linearGradient>
<linearGradient id="spiderCurrentGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#f59e0b" stopOpacity="0.45" />
<stop offset="100%" stopColor="#ef4444" stopOpacity="0.2" />
</linearGradient>
</defs>
{/* Concentric Web Grids */}
{gridLevels.map((lvl) => {
const gridValues = Array(numAxes).fill(lvl);
const pts = getSpiderCoordinates(gridValues, 10, center, maxRadius);
const path = pointsToSvgPath(pts);
return (
<g key={lvl}>
<path
d={path}
fill={lvl === 10 ? 'rgba(241, 245, 249, 0.5)' : 'none'}
stroke="#e2e8f0"
strokeWidth={lvl === 10 ? '1.5' : '1'}
strokeDasharray={lvl === 10 ? 'none' : '3,3'}
/>
<text
x={center + 5}
y={center - (lvl / 10) * maxRadius + 11}
fill="#94a3b8"
fontSize="9"
fontWeight="600"
>
{toPersianDigits(lvl)}
</text>
</g>
);
})}
{/* Radial Axis Lines */}
{dimensions.map((dim, idx) => {
const angle = (2 * Math.PI * idx) / numAxes - Math.PI / 2;
const endX = center + maxRadius * Math.cos(angle);
const endY = center + maxRadius * Math.sin(angle);
// Label positions (slightly outside maxRadius)
const labelDist = maxRadius + 36;
const lx = center + labelDist * Math.cos(angle);
const ly = center + labelDist * Math.sin(angle);
const isHovered = hoveredIdx === idx;
return (
<g key={dim.id}>
<line
x1={center}
y1={center}
x2={endX}
y2={endY}
stroke="#e2e8f0"
strokeWidth="1.2"
/>
{/* Interactive Label on Axis End */}
<g
transform={`translate(${lx}, ${ly})`}
className="cursor-pointer"
onClick={() => onSelectDimension && onSelectDimension(dim)}
onMouseEnter={() => setHoveredIdx(idx)}
onMouseLeave={() => setHoveredIdx(null)}
>
<rect
x="-50"
y="-12"
width="100"
height="24"
rx="8"
fill={isHovered ? '#e6f7f3' : '#ffffff'}
stroke={isHovered ? '#00b894' : '#cbd5e1'}
strokeWidth="1.2"
filter="drop-shadow(0 1px 3px rgba(0,0,0,0.06))"
/>
<text
textAnchor="middle"
y="4"
fill={isHovered ? '#008f73' : '#1e293b'}
fontSize="9.5"
fontWeight={isHovered ? '900' : '700'}
>
{dim.shortTag}
</text>
</g>
</g>
);
})}
{/* Target 1415 Polygon (دانشگاه اصفهان ۱۴۱۵) */}
<path
d={targetPath}
fill="url(#spiderTargetGrad)"
stroke="#00b894"
strokeWidth="2.5"
className="transition-all duration-300"
/>
{/* Current State Polygon (وضع موجود) */}
<path
d={currentPath}
fill="url(#spiderCurrentGrad)"
stroke="#f59e0b"
strokeWidth="2.5"
className="transition-all duration-300"
/>
{/* Target 1415 Vertex Dots */}
{targetPoints.map((pt, idx) => (
<circle
key={`target-pt-${idx}`}
cx={pt.x}
cy={pt.y}
r="4.5"
fill="#00b894"
stroke="#ffffff"
strokeWidth="2"
className="hover:scale-150 transition-transform cursor-pointer"
onMouseEnter={() => setHoveredIdx(idx)}
onMouseLeave={() => setHoveredIdx(null)}
/>
))}
{/* Current State Vertex Dots */}
{currentPoints.map((pt, idx) => (
<circle
key={`curr-pt-${idx}`}
cx={pt.x}
cy={pt.y}
r="4.5"
fill="#f59e0b"
stroke="#ffffff"
strokeWidth="2"
className="hover:scale-150 transition-transform cursor-pointer"
onMouseEnter={() => setHoveredIdx(idx)}
onMouseLeave={() => setHoveredIdx(null)}
/>
))}
{/* Center Hub Marker */}
<circle cx={center} cy={center} r="4" fill="#64748b" />
</svg>
{/* Interactive Hover Tooltip (Light Bento Card) */}
{hoveredIdx !== null && (
<div className="absolute top-4 left-4 z-40 bg-white border border-slate-200 p-4 rounded-2xl shadow-xl max-w-xs text-right animate-fadeIn">
<div className="flex items-center justify-between mb-1.5">
<span className="text-xs font-black text-slate-900">{dimensions[hoveredIdx].title}</span>
<span
className="text-[10px] font-bold px-2 py-0.5 rounded-full"
style={{
backgroundColor: `${dimensions[hoveredIdx].pillarColor}15`,
color: dimensions[hoveredIdx].pillarColor
}}
>
آمادگی: {toPersianDigits(dimensions[hoveredIdx].readinessPercentage)}٪
</span>
</div>
<div className="flex items-center justify-between text-xs my-2 bg-slate-50 p-2.5 rounded-xl border border-slate-100 font-bold">
<span className="text-amber-600 font-bold">
وضع موجود: {toPersianDigits(dimensions[hoveredIdx].currentScore)}
</span>
<span className="text-teal-600 font-bold">
هدف ۱۴۱۵: {toPersianDigits(dimensions[hoveredIdx].targetScore)}
</span>
<span className="text-rose-600 font-extrabold">
شکاف: {toPersianDigits(dimensions[hoveredIdx].gap)}
</span>
</div>
<p className="text-[11px] text-slate-500 line-clamp-2 leading-relaxed">
{dimensions[hoveredIdx].vision}
</p>
</div>
)}
{/* Spider Chart Legend */}
<div className="flex items-center gap-6 mt-4 text-xs font-bold">
<div className="flex items-center gap-2">
<span className="w-3.5 h-3.5 rounded-full bg-teal-500 border border-white shadow-sm"></span>
<span className="text-slate-800">هدف دانشگاه اصفهان ۱۴۱۵ (وضع مطلوب)</span>
</div>
<div className="flex items-center gap-2">
<span className="w-3.5 h-3.5 rounded-full bg-amber-500 border border-white shadow-sm"></span>
<span className="text-slate-800">وضعیت موجود دانشگاه (مبنای سنجش)</span>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,238 @@
import React, { useState } from 'react';
import { IntelligenceItem, University1415Dimension, JobEntity, SkillEntity } from '../../types/futures';
import { toPersianDigits } from '../../utils/persianNumbers';
import { ArrowLeft, GitFork, Cpu, GraduationCap, Briefcase, Target } from 'lucide-react';
interface StrategicGraphProps {
items: IntelligenceItem[];
dimensions: University1415Dimension[];
jobs: JobEntity[];
skills: SkillEntity[];
onSelectEntity?: (type: string, id: string) => void;
}
export const StrategicGraph: React.FC<StrategicGraphProps> = ({
items,
dimensions,
jobs,
skills,
onSelectEntity
}) => {
// Pre-configured end-to-end strategic disruption pathways for demonstration
const scenarios = [
{
id: 'sc-1',
title: 'زنجیره هوش مصنوعی مولد و دانشگاه هوشمند (AI-Native Disruption)',
tech: items.find((i) => i.id === 'tc-1') || items[0],
trend: items.find((i) => i.id === 'tr-1') || items[0],
skill: skills.find((s) => s.id === 'sk-1') || skills[0],
job: jobs.find((j) => j.id === 'job-1') || jobs[0],
dimension: dimensions.find((d) => d.id === 'dim-1') || dimensions[0],
gapAnalysis: 'شکاف ۵.۳ نمره‌ای در آمادگی زیرساخت هوش مصنوعی و نیاز به ۳۰ سرور پردازش گرافیکی بومی.'
},
{
id: 'sc-2',
title: 'زنجیره بحران آب، فرونشست زمین و دانشگاه مسئله‌محور (Water & Subsidence Mission)',
tech: items.find((i) => i.id === 'tc-12') || items[1],
trend: items.find((i) => i.id === 'tr-10') || items[1],
skill: skills.find((s) => s.id === 'sk-4') || skills[1],
job: jobs.find((j) => j.id === 'job-16') || jobs[1],
dimension: dimensions.find((d) => d.id === 'dim-10') || dimensions[1],
gapAnalysis: 'شکاف ۴.۴ نمره‌ای در پژوهش‌های مأموریت‌گرا و فقدان آزمایشگاه متمرکز پایش فرونشست.'
},
{
id: 'sc-3',
title: 'زنجیره ریزمدرک‌ها، دوره‌های صنعتی Co-op و دانشگاه مهارت‌محور (Skills & Co-op Pathway)',
tech: items.find((i) => i.id === 'tc-5') || items[2],
trend: items.find((i) => i.id === 'tr-2') || items[2],
skill: skills.find((s) => s.id === 'sk-8') || skills[2],
job: jobs.find((j) => j.id === 'job-2') || jobs[2],
dimension: dimensions.find((d) => d.id === 'dim-3') || dimensions[2],
gapAnalysis: 'شکاف ۴.۶ نمره‌ای در آموزش‌های مبتنی بر شایستگی و مقاومت در حذف دروس تئوری غیرکاربردی.'
}
];
const [activeScenarioIdx, setActiveScenarioIdx] = useState(0);
const activeScenario = scenarios[activeScenarioIdx];
return (
<div className="w-full flex flex-col gap-6">
{/* Scenario Selector Header */}
<div className="flex flex-wrap items-center justify-between gap-3 bg-[#0d162a] border border-slate-800 p-4 rounded-xl">
<div className="flex items-center gap-2 text-white font-bold text-sm">
<GitFork size={18} className="text-teal-400" />
<span>زنجیره ارزش تحول راهبردی:</span>
</div>
<div className="flex flex-wrap gap-2">
{scenarios.map((sc, idx) => (
<button
key={sc.id}
onClick={() => setActiveScenarioIdx(idx)}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
activeScenarioIdx === idx
? 'bg-teal-500 text-slate-950 shadow-md shadow-teal-500/20 font-bold'
: 'bg-slate-800 text-slate-300 hover:bg-slate-700'
}`}
>
{sc.title.split(' ')[1]} {sc.title.split(' ')[2]}
</button>
))}
</div>
</div>
{/* Disruption Chain Visual Flow */}
<div className="intel-card border-slate-700/80 p-6 lg:p-8 flex flex-col gap-6">
<div className="border-b border-slate-800 pb-3">
<h3 className="text-white font-black text-base lg:text-lg">
{activeScenario.title}
</h3>
<p className="text-slate-400 text-xs mt-1">
ردگیری مسیر تبدیل سیگنال/فناوری بیرونی به شکاف قابلیت در دانشگاه اصفهان
</p>
</div>
{/* Linear Step Flow Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-3 relative">
{/* Step 1: Technology / Signal */}
<div
className="bg-[#091122] border border-teal-500/40 p-4 rounded-xl flex flex-col justify-between cursor-pointer hover:border-teal-400 transition-all"
onClick={() => onSelectEntity && onSelectEntity('item', activeScenario.tech.id)}
>
<div>
<div className="flex items-center justify-between text-[11px] text-teal-400 font-bold mb-2">
<span className="flex items-center gap-1">
<Cpu size={13} />
۱. پیشران فناورانه
</span>
<span className="badge badge-teal">محرک بیرونی</span>
</div>
<h5 className="text-white font-bold text-xs leading-snug mb-1">
{activeScenario.tech.title}
</h5>
<p className="text-slate-400 text-[11px] line-clamp-3">
{activeScenario.tech.executiveSummary}
</p>
</div>
<div className="pt-2 mt-3 border-t border-slate-800/80 text-[10px] text-slate-500">
افق: {toPersianDigits(activeScenario.tech.horizon)} | تأثیر: {toPersianDigits(activeScenario.tech.impactScore)} از ۵
</div>
</div>
{/* Step 2: Educational Disruption */}
<div
className="bg-[#091122] border border-sky-500/40 p-4 rounded-xl flex flex-col justify-between cursor-pointer hover:border-sky-400 transition-all"
onClick={() => onSelectEntity && onSelectEntity('item', activeScenario.trend.id)}
>
<div>
<div className="flex items-center justify-between text-[11px] text-sky-400 font-bold mb-2">
<span className="flex items-center gap-1">
<GraduationCap size={13} />
۲. اختلال آموزشی و پژوهشی
</span>
<span className="badge badge-blue">روند تغییر</span>
</div>
<h5 className="text-white font-bold text-xs leading-snug mb-1">
{activeScenario.trend.title}
</h5>
<p className="text-slate-400 text-[11px] line-clamp-3">
{activeScenario.trend.whyItMatters}
</p>
</div>
<div className="pt-2 mt-3 border-t border-slate-800/80 text-[10px] text-slate-500">
تغییر مدل آموزش و ارزشیابی
</div>
</div>
{/* Step 3: Required Skills */}
<div className="bg-[#091122] border border-purple-500/40 p-4 rounded-xl flex flex-col justify-between">
<div>
<div className="flex items-center justify-between text-[11px] text-purple-400 font-bold mb-2">
<span className="flex items-center gap-1">
<ArrowLeft size={13} />
۳. شایستگی نوظهور
</span>
<span className="badge badge-purple">مهارت الزامی</span>
</div>
<h5 className="text-white font-bold text-xs leading-snug mb-1">
{activeScenario.skill.title}
</h5>
<p className="text-slate-400 text-[11px] line-clamp-3">
{activeScenario.skill.description}
</p>
</div>
<div className="pt-2 mt-3 border-t border-slate-800/80 text-[10px] text-slate-500">
رده: {activeScenario.skill.category}
</div>
</div>
{/* Step 4: Transformed Jobs */}
<div className="bg-[#091122] border border-amber-500/40 p-4 rounded-xl flex flex-col justify-between">
<div>
<div className="flex items-center justify-between text-[11px] text-amber-400 font-bold mb-2">
<span className="flex items-center gap-1">
<Briefcase size={13} />
۴. بازار کار آینده
</span>
<span className="badge badge-amber">مشاغل هدف</span>
</div>
<h5 className="text-white font-bold text-xs leading-snug mb-1">
{activeScenario.job.title}
</h5>
<p className="text-slate-400 text-[11px] line-clamp-3">
{activeScenario.job.description}
</p>
</div>
<div className="pt-2 mt-3 border-t border-slate-800/80 text-[10px] text-slate-500">
رشته: {activeScenario.job.discipline}
</div>
</div>
{/* Step 5: University 1415 Capability Gap */}
<div
className="bg-[#091122] border-2 border-rose-500/60 p-4 rounded-xl flex flex-col justify-between cursor-pointer hover:border-rose-400 transition-all bg-rose-950/10"
onClick={() => onSelectEntity && onSelectEntity('dimension', activeScenario.dimension.id)}
>
<div>
<div className="flex items-center justify-between text-[11px] text-rose-400 font-bold mb-2">
<span className="flex items-center gap-1">
<Target size={13} />
۵. شکاف قابلیت ۱۴۱۵
</span>
<span className="badge badge-rose">نقطه مداخله</span>
</div>
<h5 className="text-white font-bold text-xs leading-snug mb-1">
{activeScenario.dimension.title}
</h5>
<p className="text-rose-200/90 text-[11px] font-medium leading-relaxed">
{activeScenario.gapAnalysis}
</p>
</div>
<div className="pt-2 mt-3 border-t border-slate-800/80 flex items-center justify-between text-[11px] text-rose-300 font-bold">
<span>وضع موجود: {toPersianDigits(activeScenario.dimension.currentScore)}</span>
<span>هدف: {toPersianDigits(activeScenario.dimension.targetScore)}</span>
</div>
</div>
</div>
{/* Strategic Takeaway Memo Box */}
<div className="bg-[#0c1833] border border-teal-500/30 rounded-xl p-4 flex flex-col md:flex-row items-start md:items-center justify-between gap-4 text-xs">
<div>
<span className="text-teal-400 font-bold block mb-0.5">
نتیجهگیری سیاستی برای هیئت رئیسه دانشگاه اصفهان:
</span>
<p className="text-slate-300 leading-relaxed">
این پیشران بیرونی در صورت انفعال دانشگاه، به تهدیدی برای تعطیلی گرایشهای مرتبط و افت جذب دانشجو تبدیل خواهد شد؛ اما با بازنگری هدفمند سرفصلها و تجهیز آزمایشگاهها میتواند به مزیت رقابتی دانشگاه اصفهان در منطقه مرکزی تبدیل شود.
</p>
</div>
<button
onClick={() => onSelectEntity && onSelectEntity('dimension', activeScenario.dimension.id)}
className="btn btn-primary text-xs py-2 px-3.5 self-end md:self-auto shrink-0"
>
مشاهده برنامه اقدام بعد ۱۴۱۵
</button>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,404 @@
import React, { useState } from 'react';
import { FuturesForce, ForceType } from '../../types/futures';
import { toPersianDigits } from '../../utils/persianNumbers';
import {
Compass,
ArrowUpRight,
ShieldAlert,
Sparkles,
TrendingUp,
Target
} from 'lucide-react';
interface TriangleViewProps {
forces: FuturesForce[];
onSelectForce?: (force: FuturesForce) => void;
}
export const TriangleView: React.FC<TriangleViewProps> = ({ forces, onSelectForce }) => {
const [selectedForce, setSelectedForce] = useState<FuturesForce>(forces[0]);
const [activeTab, setActiveTab] = useState<ForceType>('pull');
const pullForces = forces.filter((f) => f.forceType === 'pull');
const pushForces = forces.filter((f) => f.forceType === 'push');
const weightForces = forces.filter((f) => f.forceType === 'weight');
const handleForceClick = (f: FuturesForce) => {
setSelectedForce(f);
setActiveTab(f.forceType);
if (onSelectForce) onSelectForce(f);
};
// Sleek Equilateral Geometry in 800x560 coordinate space
// Base: 650 - 150 = 500, Height: 480 - 50 = 430 (Proportion ~0.86, perfectly sleek)
const apex = { x: 400, y: 50 };
const rightCorner = { x: 650, y: 480 };
const leftCorner = { x: 150, y: 480 };
const centroid = { x: 400, y: 336 };
return (
<div className="w-full flex flex-col gap-6 animate-fadeIn">
{/* Visual Triangle Composition Canvas */}
<div className="relative w-full bg-white border border-slate-200 rounded-3xl p-6 lg:p-8 shadow-bento flex flex-col items-center">
{/* Subtle Ambient Background Gradients */}
<div className="absolute top-6 left-1/2 -translate-x-1/2 w-[400px] h-[200px] bg-purple-500/5 rounded-full blur-3xl pointer-events-none"></div>
<div className="absolute bottom-12 right-12 w-64 h-64 bg-sky-500/5 rounded-full blur-3xl pointer-events-none"></div>
<div className="absolute bottom-12 left-12 w-64 h-64 bg-rose-500/5 rounded-full blur-3xl pointer-events-none"></div>
{/* 1. TOP VERTEX APEX CLUSTER: PULL OF THE FUTURE (کشش آینده) */}
<div className="w-full flex flex-col items-center mb-3 z-10">
<div className="flex items-center gap-2 bg-purple-50 border border-purple-200 px-4 py-1.5 rounded-full shadow-sm text-purple-950 font-black text-xs">
<Sparkles size={16} className="text-purple-600" />
<span>کشش آینده تصویر آرمانی دانشگاه</span>
</div>
<span className="text-[11px] text-slate-500 font-semibold mt-1">
جاذبهها، چشماندازهای نو و الگوهای مطلوب آینده
</span>
{/* Pull Force Pills */}
<div className="flex flex-wrap justify-center gap-2 mt-2.5 max-w-xl">
{pullForces.map((f) => {
const isSelected = selectedForce.id === f.id;
return (
<button
key={f.id}
onClick={() => handleForceClick(f)}
className={`px-3.5 py-1.5 rounded-xl text-xs font-bold transition-all flex items-center gap-1.5 shadow-sm ${
isSelected
? 'bg-slate-900 text-white shadow-md scale-105'
: 'bg-white text-slate-800 hover:bg-purple-50 hover:text-purple-900 border border-purple-200'
}`}
>
<span>{f.title}</span>
<span className={`text-[10px] px-1.5 py-0.2 rounded-full font-bold ${
isSelected ? 'bg-slate-800 text-purple-300' : 'bg-purple-100 text-purple-800'
}`}>
{toPersianDigits(f.strength)}
</span>
</button>
);
})}
</div>
</div>
{/* 2. SLEEK PROPORTIONAL GEOMETRIC TRIANGLE SVG CANVAS */}
<div className="relative w-full max-w-[720px] my-2 select-none">
<svg
viewBox="0 0 800 550"
className="w-full h-auto drop-shadow-sm"
style={{ overflow: 'visible' }}
>
<defs>
{/* Triangular Gradient Fill */}
<linearGradient id="futuresTriangleGrad" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stopColor="#8b5cf6" stopOpacity="0.12" />
<stop offset="60%" stopColor="#0284c7" stopOpacity="0.08" />
<stop offset="100%" stopColor="#f43f5e" stopOpacity="0.12" />
</linearGradient>
{/* Edge Gradient: Left (PULL -> WEIGHT) */}
<linearGradient id="pullToWeightGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#8b5cf6" />
<stop offset="100%" stopColor="#f43f5e" />
</linearGradient>
{/* Edge Gradient: Right (PULL -> PUSH) */}
<linearGradient id="pullToPushGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#8b5cf6" />
<stop offset="100%" stopColor="#0284c7" />
</linearGradient>
{/* Edge Gradient: Bottom (WEIGHT <-> PUSH) */}
<linearGradient id="weightToPushGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#f43f5e" />
<stop offset="100%" stopColor="#0284c7" />
</linearGradient>
{/* Glow filter */}
<filter id="nodeShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" floodColor="#0f172a" floodOpacity="0.1" />
</filter>
</defs>
{/* REAL SOLID SLEEK GEOMETRIC TRIANGLE */}
<polygon
points={`${apex.x},${apex.y} ${rightCorner.x},${rightCorner.y} ${leftCorner.x},${leftCorner.y}`}
fill="url(#futuresTriangleGrad)"
stroke="#cbd5e1"
strokeWidth="2.5"
strokeLinejoin="round"
/>
{/* INNER EQUILIBRIUM DASHED TRIANGLE */}
<polygon
points={`${apex.x},${apex.y + 60} ${rightCorner.x - 55},${rightCorner.y - 35} ${leftCorner.x + 55},${leftCorner.y - 35}`}
fill="none"
stroke="#94a3b8"
strokeWidth="1.2"
strokeDasharray="5,5"
opacity="0.6"
/>
{/* CENTROID TETHERS (Lines from centroid to 3 corners) */}
<line
x1={centroid.x}
y1={centroid.y}
x2={apex.x}
y2={apex.y}
stroke="#8b5cf6"
strokeWidth="2"
strokeDasharray="4,4"
opacity="0.8"
/>
<line
x1={centroid.x}
y1={centroid.y}
x2={rightCorner.x}
y2={rightCorner.y}
stroke="#0284c7"
strokeWidth="2"
strokeDasharray="4,4"
opacity="0.8"
/>
<line
x1={centroid.x}
y1={centroid.y}
x2={leftCorner.x}
y2={leftCorner.y}
stroke="#f43f5e"
strokeWidth="2"
strokeDasharray="4,4"
opacity="0.8"
/>
{/* PROMINENT TRIANGLE EDGES (SLEEK & SOLID) */}
{/* Right Edge: PULL to PUSH */}
<line
x1={apex.x}
y1={apex.y}
x2={rightCorner.x}
y2={rightCorner.y}
stroke="url(#pullToPushGrad)"
strokeWidth="4"
strokeLinecap="round"
/>
{/* Left Edge: PULL to WEIGHT */}
<line
x1={apex.x}
y1={apex.y}
x2={leftCorner.x}
y2={leftCorner.y}
stroke="url(#pullToWeightGrad)"
strokeWidth="4"
strokeLinecap="round"
/>
{/* Bottom Edge: WEIGHT to PUSH */}
<line
x1={leftCorner.x}
y1={leftCorner.y}
x2={rightCorner.x}
y2={rightCorner.y}
stroke="url(#weightToPushGrad)"
strokeWidth="4"
strokeLinecap="round"
/>
{/* Edge Vector Dynamics Badges */}
{/* Right edge midpoint */}
<g transform={`translate(${(apex.x + rightCorner.x) / 2 + 35}, ${(apex.y + rightCorner.y) / 2}) rotate(60)`}>
<rect x="-65" y="-11" width="130" height="22" rx="11" fill="#ffffff" stroke="#c8e0fd" strokeWidth="1" filter="url(#nodeShadow)" />
<text textAnchor="middle" y="4" fill="#0284c7" fontSize="10" fontWeight="800">
پویش روندهای زمان حال
</text>
</g>
{/* Left edge midpoint */}
<g transform={`translate(${(apex.x + leftCorner.x) / 2 - 35}, ${(apex.y + leftCorner.y) / 2}) rotate(-60)`}>
<rect x="-65" y="-11" width="130" height="22" rx="11" fill="#ffffff" stroke="#f9cbd9" strokeWidth="1" filter="url(#nodeShadow)" />
<text textAnchor="middle" y="4" fill="#e11d48" fontSize="10" fontWeight="800">
مهار توسط لنگرهای گذشته
</text>
</g>
{/* Bottom edge midpoint */}
<g transform={`translate(${centroid.x}, ${leftCorner.y + 24})`}>
<rect x="-90" y="-12" width="180" height="24" rx="12" fill="#ffffff" stroke="#e2e8f0" strokeWidth="1" filter="url(#nodeShadow)" />
<text textAnchor="middle" y="4.5" fill="#334155" fontSize="10.5" fontWeight="800">
تعارض لختی سنتی و شتاب فناوری
</text>
</g>
{/* CENTRAL HUB: آینده دانشگاه اصفهان (At Centroid) */}
<g transform={`translate(${centroid.x}, ${centroid.y})`}>
<circle r="44" fill="#ffffff" stroke="#00b894" strokeWidth="3" filter="url(#nodeShadow)" />
<circle r="36" fill="#e6f7f3" />
<circle r="12" fill="#00b894" className="anim-pulse" opacity="0.3" />
<circle r="6" fill="#00b894" />
<text textAnchor="middle" y="-12" fill="#0f172a" fontSize="11" fontWeight="900">
دانشگاه اصفهان
</text>
<text textAnchor="middle" y="24" fill="#008f73" fontSize="9.5" fontWeight="800">
افق ۱۴۱۵
</text>
</g>
{/* VERTEX CORNER NODES */}
{/* Top Vertex (PULL) */}
<g transform={`translate(${apex.x}, ${apex.y})`}>
<circle r="20" fill="#ffffff" stroke="#8b5cf6" strokeWidth="3.5" filter="url(#nodeShadow)" />
<circle r="9" fill="#8b5cf6" />
</g>
{/* Bottom-Right Vertex (PUSH) */}
<g transform={`translate(${rightCorner.x}, ${rightCorner.y})`}>
<circle r="20" fill="#ffffff" stroke="#0284c7" strokeWidth="3.5" filter="url(#nodeShadow)" />
<circle r="9" fill="#0284c7" />
</g>
{/* Bottom-Left Vertex (WEIGHT) */}
<g transform={`translate(${leftCorner.x}, ${leftCorner.y})`}>
<circle r="20" fill="#ffffff" stroke="#f43f5e" strokeWidth="3.5" filter="url(#nodeShadow)" />
<circle r="9" fill="#f43f5e" />
</g>
</svg>
</div>
{/* 3. BOTTOM TWO CORNER PANELS (PUSH & WEIGHT) */}
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-5 mt-2 pt-4 border-t border-slate-100 z-10">
{/* PUSH PANEL (Bottom-Right Vertex) */}
<div className="flex flex-col items-center md:items-start text-center md:text-right p-4 bg-sky-50/60 border border-sky-200 rounded-2xl">
<div className="flex items-center gap-2 bg-white border border-sky-300 px-3.5 py-1.5 rounded-full shadow-sm text-sky-950 font-black text-xs mb-1">
<TrendingUp size={15} className="text-sky-600" />
<span>فشارهای زمان حال</span>
</div>
<span className="text-[11px] text-slate-500 font-semibold mb-2.5">
روندها، فناوریهای فعال و تحولات جاری
</span>
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
{pushForces.map((f) => {
const isSelected = selectedForce.id === f.id;
return (
<button
key={f.id}
onClick={() => handleForceClick(f)}
className={`px-3.5 py-1.5 rounded-xl text-xs font-bold transition-all flex items-center gap-1.5 shadow-sm ${
isSelected
? 'bg-slate-900 text-white shadow-md scale-105'
: 'bg-white text-slate-800 hover:bg-sky-100 hover:text-sky-900 border border-sky-200'
}`}
>
<span>{f.title}</span>
<span className={`text-[10px] px-1.5 py-0.2 rounded-full font-bold ${
isSelected ? 'bg-slate-800 text-sky-300' : 'bg-sky-100 text-sky-800'
}`}>
{toPersianDigits(f.strength)}
</span>
</button>
);
})}
</div>
</div>
{/* WEIGHT PANEL (Bottom-Left Vertex) */}
<div className="flex flex-col items-center md:items-end text-center md:text-left p-4 bg-rose-50/60 border border-rose-200 rounded-2xl">
<div className="flex items-center gap-2 bg-white border border-rose-300 px-3.5 py-1.5 rounded-full shadow-sm text-rose-950 font-black text-xs mb-1">
<ShieldAlert size={15} className="text-rose-600" />
<span>وزن گذشته</span>
</div>
<span className="text-[11px] text-slate-500 font-semibold mb-2.5">
موانع ساختاری، آییننامهها و لختی سازمانی
</span>
<div className="flex flex-wrap gap-2 justify-center md:justify-end">
{weightForces.map((f) => {
const isSelected = selectedForce.id === f.id;
return (
<button
key={f.id}
onClick={() => handleForceClick(f)}
className={`px-3.5 py-1.5 rounded-xl text-xs font-bold transition-all flex items-center gap-1.5 shadow-sm ${
isSelected
? 'bg-slate-900 text-white shadow-md scale-105'
: 'bg-white text-slate-800 hover:bg-rose-100 hover:text-rose-900 border border-rose-200'
}`}
>
<span>{f.title}</span>
<span className={`text-[10px] px-1.5 py-0.2 rounded-full font-bold ${
isSelected ? 'bg-slate-800 text-rose-300' : 'bg-rose-100 text-rose-800'
}`}>
{toPersianDigits(f.strength)}
</span>
</button>
);
})}
</div>
</div>
</div>
</div>
{/* Selected Force Intelligence Dossier Card */}
{selectedForce && (
<div className="intel-card bg-white border border-slate-200 rounded-3xl p-6 shadow-sm flex flex-col gap-4 animate-fadeIn">
<div className="flex flex-wrap items-center justify-between gap-3 pb-3 border-b border-slate-100">
<div className="flex items-center gap-3">
<span
className="w-3.5 h-3.5 rounded-full shadow-sm"
style={{
backgroundColor:
selectedForce.forceType === 'pull'
? '#8b5cf6'
: selectedForce.forceType === 'push'
? '#0284c7'
: '#f43f5e'
}}
></span>
<h3 className="text-slate-900 font-black text-lg">{selectedForce.title}</h3>
<span
className={`badge ${
selectedForce.forceType === 'pull'
? 'badge-purple'
: selectedForce.forceType === 'push'
? 'badge-blue'
: 'badge-rose'
}`}
>
{selectedForce.forceType === 'pull'
? 'کشش آینده'
: selectedForce.forceType === 'push'
? 'فشار زمان حال'
: 'وزن گذشته'}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 font-bold">شدت اثر نیرو:</span>
<span className="text-lg font-black text-slate-900 bg-slate-100 px-3 py-0.5 rounded-full border border-slate-200">
{toPersianDigits(selectedForce.strength)} از ۱۰
</span>
</div>
</div>
<div className="bg-slate-50 border border-slate-200 p-3.5 rounded-2xl text-xs text-slate-700 leading-relaxed">
<span className="text-slate-900 font-bold block mb-1">شواهد و مؤیدهای تجربی:</span>
{selectedForce.evidence}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs mt-1">
<div className="p-4 bg-slate-50 border border-slate-200 rounded-2xl">
<span className="text-slate-900 font-bold block mb-1">پیامد راهبردی برای دانشگاه اصفهان:</span>
<p className="text-slate-600 leading-relaxed text-[11.5px]">{selectedForce.impact}</p>
</div>
<div className="p-4 bg-teal-50 border border-teal-200 rounded-2xl">
<span className="text-teal-900 font-bold block mb-1">اقدام پیشنهادی جهت غلبه / همافزایی:</span>
<p className="text-teal-800 leading-relaxed text-[11.5px] font-medium">{selectedForce.strategicImplication}</p>
</div>
</div>
</div>
)}
</div>
);
};

3680
src/data/seedData.ts Normal file

File diff suppressed because it is too large Load Diff

892
src/index.css Normal file
View File

@ -0,0 +1,892 @@
/* =========================================================================
University Futures Radar Design System (Bento & Neo-Clean Light Theme)
Inspired by modern Dribbble & Bento institutional foresight dashboards
University of Isfahan (دانشگاه اصفهان)
========================================================================= */
:root {
/* Canvas & Backgrounds */
--bg-canvas: #f4f6fb;
--bg-card: #ffffff;
--bg-card-subtle: #f8fafc;
--bg-sidebar: #ffffff;
--bg-header: rgba(255, 255, 255, 0.88);
/* Borders & Dividers */
--border-card: #e5eaf2;
--border-subtle: #edf2f7;
--border-focus: #cbd5e1;
/* Typography Colors */
--text-main: #0f172a;
--text-heading: #1e293b;
--text-body: #475569;
--text-muted: #64748b;
--text-dim: #94a3b8;
/* Pastel Card Accent Tints (matching inspiration images) */
--pastel-mint-bg: #e6f6f1;
--pastel-mint-border: #c4ecdf;
--pastel-mint-text: #059669;
--pastel-pink-bg: #fdecf2;
--pastel-pink-border: #f9cbd9;
--pastel-pink-text: #e11d48;
--pastel-yellow-bg: #fef7e6;
--pastel-yellow-border: #fde8b8;
--pastel-yellow-text: #d97706;
--pastel-purple-bg: #f3effe;
--pastel-purple-border: #ded2fd;
--pastel-purple-text: #7c3aed;
--pastel-blue-bg: #eaf3fe;
--pastel-blue-border: #c8e0fd;
--pastel-blue-text: #0284c7;
/* Brand Accents */
--accent-teal: #00b894;
--accent-teal-dark: #008f73;
--accent-dark-pill: #0f172a;
/* Radii */
--radius-xs: 6px;
--radius-sm: 10px;
--radius-md: 14px;
--radius-lg: 20px;
--radius-xl: 26px;
--radius-2xl: 32px;
/* Shadows */
--shadow-subtle: 0 2px 8px -2px rgba(15, 23, 42, 0.04);
--shadow-bento: 0 10px 30px -4px rgba(15, 23, 42, 0.05), 0 2px 6px -1px rgba(15, 23, 42, 0.02);
--shadow-elevated: 0 18px 40px -6px rgba(15, 23, 42, 0.08), 0 4px 12px -2px rgba(15, 23, 42, 0.03);
--shadow-dropdown: 0 20px 45px -8px rgba(15, 23, 42, 0.14);
}
*, ::before, ::after {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', Tahoma, sans-serif !important;
}
html, body {
width: 100%;
min-height: 100%;
background-color: var(--bg-canvas);
color: #0f172a;
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', Tahoma, sans-serif !important;
direction: rtl;
text-align: right;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.6;
}
/* Strict Heading & Typography Hierarchy */
h1, h2, h3, h4, .heading-1, .heading-2, .heading-3, .heading-4, .page-title {
color: #0f172a !important;
font-weight: 800;
letter-spacing: -0.015em;
}
h1, .heading-1, .page-title {
font-size: 1.75rem !important;
font-weight: 900 !important;
line-height: 1.3 !important;
}
h2, .heading-2 {
font-size: 1.25rem !important;
font-weight: 800 !important;
line-height: 1.35 !important;
}
h3, .heading-3 {
font-size: 1.05rem !important;
font-weight: 800 !important;
line-height: 1.4 !important;
}
h4, .heading-4 {
font-size: 0.9rem !important;
font-weight: 700 !important;
line-height: 1.45 !important;
}
.text-display {
font-size: 2.25rem !important;
font-weight: 900 !important;
color: #0f172a !important;
}
.body-main {
font-size: 0.8125rem !important;
font-weight: 500 !important;
color: #334155 !important;
line-height: 1.6 !important;
}
.caption {
font-size: 0.75rem !important;
font-weight: 600 !important;
color: #64748b !important;
}
/* Numerals & Mono overrides to Vazirmatn */
.font-mono,
code,
kbd,
pre,
.kpi-number {
font-family: 'Vazirmatn', sans-serif !important;
font-feature-settings: 'ss01' 1;
}
/* =========================================================================
APP CONTAINER & SHELL LAYOUT (Bento Card Aesthetic)
========================================================================= */
.app-container {
display: grid;
grid-template-columns: 260px 1fr;
min-height: 100vh;
width: 100%;
background: radial-gradient(circle at 90% 10%, rgba(0, 184, 148, 0.06) 0%, transparent 40%),
radial-gradient(circle at 10% 90%, rgba(2, 132, 199, 0.05) 0%, transparent 40%),
var(--bg-canvas);
}
@media (max-width: 1024px) {
.app-container {
grid-template-columns: 1fr;
}
}
/* Sidebar */
aside {
width: 260px;
height: 100vh;
position: sticky;
top: 0;
background: var(--bg-sidebar);
border-left: 1px solid var(--border-card);
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 1.5rem 1.15rem;
overflow-y: auto;
z-index: 30;
}
@media (max-width: 1024px) {
aside {
display: none;
}
}
/* Main Content Area */
.main-content {
display: flex;
flex-direction: column;
min-width: 0;
overflow-x: hidden;
}
/* Header */
header {
height: 72px;
position: sticky;
top: 0;
z-index: 40;
background: var(--bg-header);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--border-card);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 2.25rem;
}
@media (max-width: 768px) {
header {
padding: 0 1.25rem;
height: 64px;
}
}
/* Page Wrapper */
.page-wrapper {
flex: 1;
padding: 2rem 2.25rem 3.5rem 2.25rem;
max-width: 1600px;
width: 100%;
margin: 0 auto;
}
@media (max-width: 768px) {
.page-wrapper {
padding: 1.25rem 1rem 2.5rem 1rem;
}
}
/* =========================================================================
BENTO BOX & CARD DESIGN (Pure White & Pastel Tints)
========================================================================= */
.intel-card {
background-color: var(--bg-card);
border: 1px solid var(--border-card);
border-radius: var(--radius-xl);
padding: 1.5rem;
box-shadow: var(--shadow-bento);
position: relative;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.intel-card:hover {
box-shadow: var(--shadow-elevated);
border-color: #d1d9e6;
}
.intel-card.interactive {
cursor: pointer;
}
.intel-card.interactive:hover {
transform: translateY(-3px);
border-color: #cbd5e1;
}
/* Bento Pastel Card Variants (Images 1 & 5) */
.bento-card-mint {
background-color: var(--pastel-mint-bg) !important;
border-color: var(--pastel-mint-border) !important;
}
.bento-card-pink {
background-color: var(--pastel-pink-bg) !important;
border-color: var(--pastel-pink-border) !important;
}
.bento-card-yellow {
background-color: var(--pastel-yellow-bg) !important;
border-color: var(--pastel-yellow-border) !important;
}
.bento-card-purple {
background-color: var(--pastel-purple-bg) !important;
border-color: var(--pastel-purple-border) !important;
}
.bento-card-blue {
background-color: var(--pastel-blue-bg) !important;
border-color: var(--pastel-blue-border) !important;
}
/* Circular Arrow Action Button (↗) - Core signature element from inspirations */
.circle-action-btn {
width: 38px;
height: 38px;
border-radius: 9999px;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.06);
border: 1px solid rgba(0, 0, 0, 0.04);
color: var(--text-main);
transition: all 0.2s ease;
font-size: 1.15rem;
font-weight: 700;
shrink: 0;
}
.circle-action-btn:hover {
transform: scale(1.08) translate(-1px, -1px);
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.1);
color: var(--accent-teal);
}
/* Dark Pill Toggle (Image 4 & 5 style) */
.pill-tab-group {
display: inline-flex;
align-items: center;
background: #eef2f7;
padding: 4px;
border-radius: 9999px;
gap: 4px;
}
.pill-tab-btn {
padding: 6px 16px;
border-radius: 9999px;
font-size: 0.8rem;
font-weight: 600;
color: var(--text-muted);
transition: all 0.2s ease;
}
.pill-tab-btn.active {
background: #0f172a;
color: #ffffff;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.2);
}
.pill-tab-btn:hover:not(.active) {
color: var(--text-main);
}
/* Typography */
.page-title {
font-size: 1.75rem;
font-weight: 800;
color: var(--text-heading);
letter-spacing: -0.02em;
display: flex;
align-items: center;
gap: 0.75rem;
}
.page-subtitle {
font-size: 0.92rem;
color: var(--text-muted);
margin-top: 0.25rem;
}
/* Modern Badges */
.badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.3rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 700;
line-height: 1;
white-space: nowrap;
}
.badge-teal {
background: #e6f7f3;
color: #009975;
border: 1px solid #b3ebd9;
}
.badge-blue {
background: #e9f3fe;
color: #0284c7;
border: 1px solid #c2defd;
}
.badge-amber {
background: #fef7e6;
color: #d97706;
border: 1px solid #fce8b8;
}
.badge-rose {
background: #feeef2;
color: #e11d48;
border: 1px solid #fdc8d5;
}
.badge-purple {
background: #f3effe;
color: #7c3aed;
border: 1px solid #ded2fd;
}
.badge-gray {
background: #f1f5f9;
color: #64748b;
border: 1px solid #e2e8f0;
}
.kpi-number {
font-family: 'Outfit', 'Vazirmatn', sans-serif;
font-weight: 800;
letter-spacing: -0.03em;
color: var(--text-heading);
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.6rem 1.25rem;
border-radius: var(--radius-md);
font-size: 0.88rem;
font-weight: 700;
transition: all 0.2s ease;
white-space: nowrap;
}
.btn-primary {
background: #0f172a;
color: #ffffff;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.18);
}
.btn-primary:hover {
background: #1e293b;
transform: translateY(-1px);
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.25);
}
.btn-secondary {
background: #ffffff;
border: 1px solid var(--border-card);
color: var(--text-heading);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
}
.btn-secondary:hover {
background: #f8fafc;
border-color: #cbd5e1;
}
.btn-teal {
background: linear-gradient(135deg, #00b894 0%, #0284c7 100%);
color: #ffffff;
box-shadow: 0 4px 14px rgba(0, 184, 148, 0.25);
}
.btn-teal:hover {
transform: translateY(-1px);
box-shadow: 0 6px 18px rgba(0, 184, 148, 0.35);
}
.btn-ghost {
color: var(--text-muted);
}
.btn-ghost:hover {
color: var(--text-heading);
background: #f1f5f9;
}
/* Modals */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.45);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1.5rem;
}
.modal-content {
background: #ffffff;
border: 1px solid var(--border-card);
border-radius: var(--radius-2xl);
max-width: 720px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
box-shadow: var(--shadow-dropdown);
}
/* Radar Animation */
@keyframes radar-scan {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes pulse-glow {
0%, 100% { opacity: 0.4; transform: scale(1); }
50% { opacity: 0.85; transform: scale(1.08); }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.anim-pulse { animation: pulse-glow 3s infinite ease-in-out; }
.radar-sweep-line { transform-origin: center center; animation: radar-scan 14s infinite linear; }
.animate-fadeIn { animation: fadeIn 0.25s ease-out forwards; }
/* Line Clamp */
.line-clamp-1 { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
.line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.line-clamp-3 { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
/* =========================================================================
RESPONSIVE UTILITY SUITE
========================================================================= */
.flex { display: flex; }
.inline-flex { display: inline-flex; }
.flex-col { flex-direction: column; }
.flex-row { flex-direction: row; }
.flex-wrap { flex-wrap: wrap; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.items-end { align-items: flex-end; }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
.justify-end { justify-content: flex-end; }
.flex-1 { flex: 1 1 0%; }
.shrink-0 { flex-shrink: 0; }
.self-start { align-self: flex-start; }
.self-end { align-self: flex-end; }
.self-auto { align-self: auto; }
.grid { display: grid; }
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
@media (min-width: 640px) {
.sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.sm\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.sm\:inline { display: inline; }
.sm\:inline-block { display: inline-block; }
.sm\:w-64 { width: 16rem; }
}
@media (min-width: 768px) {
.md\:flex { display: flex; }
.md\:hidden { display: none; }
.md\:block { display: block; }
.md\:inline { display: inline; }
.md\:flex-row { flex-direction: row; }
.md\:items-center { align-items: center; }
.md\:items-start { align-items: flex-start; }
.md\:items-end { align-items: flex-end; }
.md\:self-auto { align-self: auto; }
.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.md\:text-right { text-align: right; }
}
@media (min-width: 1024px) {
.lg\:flex { display: flex; }
.lg\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.lg\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.lg\:grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.lg\:grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.lg\:col-span-7 { grid-column: span 7 / span 7; }
.lg\:col-span-5 { grid-column: span 5 / span 5; }
.lg\:text-2xl { font-size: 1.5rem; line-height: 2rem; }
.lg\:text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
.lg\:p-8 { padding: 2rem; }
.lg\:p-10 { padding: 2.5rem; }
.lg\:p-12 { padding: 3rem; }
.lg\:w-72 { width: 18rem; }
}
@media (min-width: 1280px) {
.xl\:grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); }
.xl\:col-span-7 { grid-column: span 7 / span 7; }
.xl\:col-span-8 { grid-column: span 8 / span 8; }
.xl\:col-span-5 { grid-column: span 5 / span 5; }
.xl\:col-span-4 { grid-column: span 4 / span 4; }
.xl\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.xl\:grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}
/* Gaps */
.gap-1 { gap: 0.25rem; }
.gap-1\.5 { gap: 0.375rem; }
.gap-2 { gap: 0.5rem; }
.gap-2\.5 { gap: 0.625rem; }
.gap-3 { gap: 0.75rem; }
.gap-3\.5 { gap: 0.875rem; }
.gap-4 { gap: 1rem; }
.gap-5 { gap: 1.25rem; }
.gap-6 { gap: 1.5rem; }
.gap-8 { gap: 2rem; }
/* Dimensions */
.w-full { width: 100%; }
.w-auto { width: auto; }
.w-5 { width: 1.25rem; }
.h-5 { height: 1.25rem; }
.w-6 { width: 1.5rem; }
.h-6 { height: 1.5rem; }
.w-8 { width: 2rem; }
.h-8 { height: 2rem; }
.w-10 { width: 2.5rem; }
.h-10 { height: 2.5rem; }
.w-12 { width: 3rem; }
.h-12 { height: 3rem; }
.w-14 { width: 3.5rem; }
.h-14 { height: 3.5rem; }
.w-16 { width: 4rem; }
.w-24 { width: 6rem; }
.w-28 { width: 7rem; }
.w-64 { width: 16rem; }
.w-80 { width: 20rem; }
.h-1\.5 { height: 0.375rem; }
.h-2 { height: 0.5rem; }
.h-2\.5 { height: 0.625rem; }
.h-full { height: 100%; }
.h-auto { height: auto; }
.min-h-screen { min-height: 100vh; }
.max-w-xs { max-width: 20rem; }
.max-w-sm { max-width: 24rem; }
.max-w-md { max-width: 28rem; }
.max-w-2xl { max-width: 42rem; }
.max-w-3xl { max-width: 48rem; }
.max-w-4xl { max-width: 56rem; }
.max-w-5xl { max-width: 64rem; }
/* Spacing */
.p-0 { padding: 0; }
.p-1 { padding: 0.25rem; }
.p-1\.5 { padding: 0.375rem; }
.p-2 { padding: 0.5rem; }
.p-2\.5 { padding: 0.625rem; }
.p-3 { padding: 0.75rem; }
.p-3\.5 { padding: 0.875rem; }
.p-4 { padding: 1rem; }
.p-5 { padding: 1.25rem; }
.p-6 { padding: 1.5rem; }
.p-8 { padding: 2rem; }
.p-10 { padding: 2.5rem; }
.p-12 { padding: 3rem; }
.px-1 { padding-left: 0.25rem; padding-right: 0.25rem; }
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
.px-2\.5 { padding-left: 0.625rem; padding-right: 0.625rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.px-3\.5 { padding-left: 0.875rem; padding-right: 0.875rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.px-5 { padding-left: 1.25rem; padding-right: 1.25rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-0\.5 { padding-top: 0.125rem; padding-bottom: 0.125rem; }
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
.py-1\.5 { padding-top: 0.375rem; padding-bottom: 0.375rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-2\.5 { padding-top: 0.625rem; padding-bottom: 0.625rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
.py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
.pt-1 { padding-top: 0.25rem; }
.pt-1\.5 { padding-top: 0.375rem; }
.pt-2 { padding-top: 0.5rem; }
.pt-3 { padding-top: 0.75rem; }
.pt-4 { padding-top: 1rem; }
.pt-6 { padding-top: 1.5rem; }
.pb-2 { padding-bottom: 0.5rem; }
.pb-2\.5 { padding-bottom: 0.625rem; }
.pb-3 { padding-bottom: 0.75rem; }
.pb-4 { padding-bottom: 1rem; }
.pb-6 { padding-bottom: 1.5rem; }
.pr-1 { padding-right: 0.25rem; }
.pr-2 { padding-right: 0.5rem; }
.pr-3 { padding-right: 0.75rem; }
.pr-8 { padding-right: 2rem; }
.pl-2 { padding-left: 0.5rem; }
.pl-3 { padding-left: 0.75rem; }
.pl-8 { padding-left: 2rem; }
.mt-0\.5 { margin-top: 0.125rem; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 0.75rem; }
.mt-4 { margin-top: 1rem; }
.mt-6 { margin-top: 1.5rem; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-1\.5 { margin-bottom: 0.375rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-2\.5 { margin-bottom: 0.625rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-5 { margin-bottom: 1.25rem; }
.mb-6 { margin-bottom: 1.5rem; }
.ml-1 { margin-left: 0.25rem; }
.ml-2 { margin-left: 0.5rem; }
.mr-6 { margin-right: 1.5rem; }
.mx-auto { margin-left: auto; margin-right: auto; }
.mx-4 { margin-left: 1rem; margin-right: 1rem; }
/* Colors */
.text-white { color: #ffffff; }
.text-slate-900 { color: #0f172a; }
.text-slate-800 { color: #1e293b; }
.text-slate-700 { color: #334155; }
.text-slate-600 { color: #475569; }
.text-slate-500 { color: #64748b; }
.text-slate-400 { color: #94a3b8; }
.text-slate-300 { color: #cbd5e1; }
.text-slate-200 { color: #e2e8f0; }
.text-teal-600 { color: #059669; }
.text-teal-500 { color: #00b894; }
.text-teal-400 { color: #10b981; }
.text-sky-600 { color: #0284c7; }
.text-sky-500 { color: #0ea5e9; }
.text-amber-600 { color: #d97706; }
.text-amber-500 { color: #f59e0b; }
.text-rose-600 { color: #e11d48; }
.text-rose-500 { color: #f43f5e; }
.text-purple-600 { color: #7c3aed; }
.text-purple-500 { color: #8b5cf6; }
.text-emerald-600 { color: #059669; }
/* Backgrounds */
.bg-white { background-color: #ffffff; }
.bg-slate-50 { background-color: #f8fafc; }
.bg-slate-100 { background-color: #f1f5f9; }
.bg-slate-200 { background-color: #e2e8f0; }
.bg-slate-800 { background-color: #1e293b; }
.bg-slate-900 { background-color: #0f172a; }
.bg-teal-50 { background-color: #e6f7f3; }
.bg-teal-100 { background-color: #c4f1e5; }
.bg-teal-500 { background-color: #00b894; }
.bg-sky-50 { background-color: #e0f2fe; }
.bg-sky-100 { background-color: #bae6fd; }
.bg-sky-500 { background-color: #0284c7; }
.bg-amber-50 { background-color: #fef3c7; }
.bg-amber-100 { background-color: #fde68a; }
.bg-amber-500 { background-color: #f59e0b; }
.bg-rose-50 { background-color: #fee2e2; }
.bg-rose-100 { background-color: #fecdd3; }
.bg-rose-500 { background-color: #f43f5e; }
.bg-purple-50 { background-color: #f3e8ff; }
.bg-purple-100 { background-color: #e9d5ff; }
.bg-purple-500 { background-color: #8b5cf6; }
/* Borders */
.border { border: 1px solid var(--border-card); }
.border-2 { border: 2px solid var(--border-card); }
.border-r-4 { border-right: 4px solid; }
.border-t { border-top: 1px solid var(--border-card); }
.border-b { border-bottom: 1px solid var(--border-card); }
.border-r { border-right: 1px solid var(--border-card); }
.border-l { border-left: 1px solid var(--border-card); }
.border-none { border: none; }
.border-slate-100 { border-color: #f1f5f9; }
.border-slate-200 { border-color: #e2e8f0; }
.border-slate-300 { border-color: #cbd5e1; }
.border-teal-200 { border-color: #a7f3d0; }
.border-teal-300 { border-color: #6ee7b7; }
.border-teal-500 { border-color: #00b894; }
.border-sky-200 { border-color: #bae6fd; }
.border-amber-200 { border-color: #fde68a; }
.border-rose-200 { border-color: #fecdd3; }
.border-purple-200 { border-color: #e9d5ff; }
/* Radii */
.rounded { border-radius: 0.375rem; }
.rounded-md { border-radius: 0.5rem; }
.rounded-lg { border-radius: 0.75rem; }
.rounded-xl { border-radius: 1rem; }
.rounded-2xl { border-radius: 1.5rem; }
.rounded-full { border-radius: 9999px; }
/* Typography */
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
.font-extrabold { font-weight: 800; }
.font-black { font-weight: 900; }
.font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
.text-\[10px\] { font-size: 10px; }
.text-\[11px\] { font-size: 11px; }
.text-\[11\.5px\] { font-size: 11.5px; }
.text-\[12px\] { font-size: 12px; }
.text-\[13px\] { font-size: 13px; }
.text-xs { font-size: 0.75rem; line-height: 1rem; }
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
.text-base { font-size: 1rem; line-height: 1.5rem; }
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
.text-right { text-align: right; }
.text-center { text-align: center; }
.text-left { text-align: left; }
.italic { font-style: italic; }
.leading-none { line-height: 1; }
.leading-tight { line-height: 1.25; }
.leading-snug { line-height: 1.375; }
.leading-relaxed { line-height: 1.625; }
.tracking-tight { letter-spacing: -0.025em; }
.select-none { user-select: none; }
.relative { position: relative; }
.absolute { position: absolute; }
.sticky { position: sticky; }
.fixed { position: fixed; }
.inset-0 { top: 0; right: 0; bottom: 0; left: 0; }
.top-0 { top: 0; }
.right-0 { right: 0; }
.left-0 { left: 0; }
.bottom-0 { bottom: 0; }
.top-2 { top: 0.5rem; }
.top-2\.5 { top: 0.625rem; }
.right-2\.5 { right: 0.625rem; }
.right-3 { right: 0.75rem; }
.z-10 { z-index: 10; }
.z-30 { z-index: 30; }
.z-40 { z-index: 40; }
.z-50 { z-index: 50; }
.cursor-pointer { cursor: pointer; }
.outline-none { outline: none; }
.shadow-sm { box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); }
.shadow-md { box-shadow: var(--shadow-bento); }
.shadow-lg { box-shadow: var(--shadow-elevated); }
.overflow-hidden { overflow: hidden; }
.overflow-y-auto { overflow-y: auto; }
.overflow-x-auto { overflow-x: auto; }
.border-collapse { border-collapse: collapse; }
.divide-y > * + * { border-top: 1px solid #f1f5f9; }
.list-disc { list-style-type: disc; }
.list-decimal { list-style-type: decimal; }
.list-inside { list-style-position: inside; }
.transition-all { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 200ms; }
.transition-colors { transition-property: color, background-color, border-color; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
.hover\:bg-slate-50:hover { background-color: #f8fafc; }
.hover\:bg-slate-100:hover { background-color: #f1f5f9; }
.hover\:border-slate-400:hover { border-color: #94a3b8; }
.hover\:text-teal-600:hover { color: #059669; }
.hover\:text-slate-900:hover { color: #0f172a; }
.hover\:underline:hover { text-decoration: underline; }
.focus\:border-teal-500:focus { border-color: #00b894; }
/* Printable View */
@media print {
body {
background: #ffffff !important;
color: #0f172a !important;
}
.no-print {
display: none !important;
}
.app-container {
display: block !important;
}
.page-wrapper {
max-width: 100% !important;
padding: 0 !important;
}
.intel-card {
border: 1px solid #cbd5e1 !important;
background: #ffffff !important;
box-shadow: none !important;
}
}

10
src/main.tsx Normal file
View File

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

354
src/pages/AdminPage.tsx Normal file
View File

@ -0,0 +1,354 @@
import React, { useState } from 'react';
import {
IntelligenceItem,
University1415Dimension,
JobEntity,
SkillEntity,
Category
} from '../types/futures';
import { dataService } from '../services/dataService';
import { EntityEditorModal } from '../components/modals/EntityEditorModal';
import {
toPersianDigits,
getCategoryLabelFa,
getCategoryColor
} from '../utils/persianNumbers';
import {
Database,
Plus,
Edit2,
Trash2,
RotateCcw,
Download,
Upload,
Search,
CheckCircle2,
Target
} from 'lucide-react';
interface AdminPageProps {
items: IntelligenceItem[];
dimensions: University1415Dimension[];
jobs: JobEntity[];
skills: SkillEntity[];
onRefreshData: () => void;
onSelectItem: (item: IntelligenceItem) => void;
}
export const AdminPage: React.FC<AdminPageProps> = ({
items,
dimensions,
jobs,
skills,
onRefreshData,
onSelectItem
}) => {
const [activeTab, setActiveTab] = useState<'items' | 'dimensions' | 'jobs'>('items');
const [selectedCategory, setSelectedCategory] = useState<Category | 'all'>('all');
const [searchQuery, setSearchQuery] = useState('');
const [isEditorOpen, setIsEditorOpen] = useState(false);
const [editingItem, setEditingItem] = useState<IntelligenceItem | null>(null);
const [successMsg, setSuccessMsg] = useState('');
const filteredItems = items.filter((item) => {
if (selectedCategory !== 'all' && item.category !== selectedCategory) return false;
if (searchQuery.trim() !== '') {
const q = searchQuery.toLowerCase().trim();
const inTitle = item.title.toLowerCase().includes(q);
const inSummary = item.executiveSummary.toLowerCase().includes(q);
if (!inTitle && !inSummary) return false;
}
return true;
});
const handleCreateNew = () => {
setEditingItem(null);
setIsEditorOpen(true);
};
const handleEdit = (item: IntelligenceItem) => {
setEditingItem(item);
setIsEditorOpen(true);
};
const handleDelete = (id: string) => {
if (window.confirm('آیا از حذف این رکورد از پایگاه داده رادار اطمینان دارید؟')) {
dataService.deleteItem(id);
onRefreshData();
showSuccess('رکورد با موفقیت حذف شد');
}
};
const handleSaveItem = (item: IntelligenceItem) => {
if (editingItem) {
dataService.updateItem(item);
showSuccess('تغییرات با موفقیت ذخیره شد');
} else {
dataService.createItem(item);
showSuccess('رکورد جدید با موفقیت به رادار اضافه شد');
}
onRefreshData();
};
const handleResetData = () => {
if (
window.confirm(
'آیا می‌خواهید داده‌های پایگاه را به داده‌های اولیه سازمانی دانشگاه اصفهان بازگردانید؟ تمامی تغییرات اختصاصی بازنشانی خواهند شد.'
)
) {
dataService.resetToDefault();
onRefreshData();
showSuccess('پایگاه داده با موفقیت به داده‌های معیار دانشگاه اصفهان بازنشانی شد');
}
};
const showSuccess = (msg: string) => {
setSuccessMsg(msg);
setTimeout(() => setSuccessMsg(''), 3000);
};
return (
<div className="flex flex-col gap-6">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-2 border-b border-slate-800">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="badge badge-purple">پنل مدیریت محتوا و دادهها</span>
<span className="text-slate-400 text-xs">عملیات CRUD و کالیبراسیون شاخصها</span>
</div>
<h1 className="page-title text-2xl text-white">
مدیریت پایگاه داده هوشمندی (Intelligence Database Studio)
</h1>
<p className="page-subtitle text-slate-300 text-xs mt-1">
ثبت، ویرایش و پایش روندها، فناوریها، سیاستها، سیگنالها، مشاغل آینده و تنظیم نمرات دانشگاه ۱۴۱۵
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleCreateNew}
className="btn btn-primary text-xs py-2 px-3.5 flex items-center gap-1.5"
>
<Plus size={15} />
<span>افزودن سیگنال / روند جدید</span>
</button>
<button
onClick={handleResetData}
className="btn btn-secondary text-xs py-2 px-3 flex items-center gap-1.5 text-rose-300 hover:text-rose-200"
title="بازنشانی به داده‌های معیار دانشگاه اصفهان"
>
<RotateCcw size={14} />
<span>بازنشانی دادهها</span>
</button>
</div>
</div>
{successMsg && (
<div className="p-3 bg-teal-950/40 border border-teal-500/50 rounded-xl text-teal-300 text-xs flex items-center gap-2 animate-fadeIn">
<CheckCircle2 size={16} className="text-teal-400" />
<span>{successMsg}</span>
</div>
)}
{/* Tabs */}
<div className="flex items-center justify-between border-b border-slate-800 pb-2">
<div className="flex items-center gap-2 text-xs">
<button
onClick={() => setActiveTab('items')}
className={`px-3 py-1.5 rounded-lg font-bold transition-all ${
activeTab === 'items'
? 'bg-teal-500 text-slate-950'
: 'text-slate-400 hover:text-white bg-[#091122]'
}`}
>
موجودیتهای رادار ({toPersianDigits(items.length)})
</button>
<button
onClick={() => setActiveTab('dimensions')}
className={`px-3 py-1.5 rounded-lg font-bold transition-all ${
activeTab === 'dimensions'
? 'bg-teal-500 text-slate-950'
: 'text-slate-400 hover:text-white bg-[#091122]'
}`}
>
ابعاد دانشگاه ۱۴۱۵ ({toPersianDigits(dimensions.length)})
</button>
<button
onClick={() => setActiveTab('jobs')}
className={`px-3 py-1.5 rounded-lg font-bold transition-all ${
activeTab === 'jobs'
? 'bg-teal-500 text-slate-950'
: 'text-slate-400 hover:text-white bg-[#091122]'
}`}
>
مشاغل و مهارتها ({toPersianDigits(jobs.length + skills.length)})
</button>
</div>
</div>
{/* Tab 1: Intelligence Items CRUD Table */}
{activeTab === 'items' && (
<div className="intel-card p-0 flex flex-col gap-4 overflow-hidden">
{/* Table Filters */}
<div className="p-4 bg-[#091122] border-b border-slate-800 flex flex-wrap items-center justify-between gap-3 text-xs">
<div className="flex flex-wrap items-center gap-2">
<span className="text-slate-400 font-semibold">فیلتر رده:</span>
{(['all', 'trend', 'technology', 'policy', 'weak_signal'] as const).map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-2.5 py-1 rounded-md transition-all ${
selectedCategory === cat
? 'bg-teal-500 text-slate-950 font-bold'
: 'bg-[#101c36] text-slate-300 hover:bg-[#16274a]'
}`}
>
{cat === 'all' ? 'همه' : getCategoryLabelFa(cat)}
</button>
))}
</div>
<div className="relative w-64">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="جستجو در عناوین..."
className="w-full bg-[#081020] border border-slate-800 rounded-lg py-1.5 pr-8 pl-2 text-xs text-white outline-none focus:border-teal-500"
/>
<Search size={13} className="absolute right-2.5 top-2 text-slate-400" />
</div>
</div>
{/* Table */}
<div className="overflow-x-auto">
<table className="w-full text-right text-xs border-collapse">
<thead>
<tr className="bg-[#0b1428] text-slate-400 border-b border-slate-800">
<th className="p-3">عنوان</th>
<th className="p-3">دستهبندی</th>
<th className="p-3">افق زمانی</th>
<th className="p-3">شدت اثر</th>
<th className="p-3">دامنه</th>
<th className="p-3">وضعیت</th>
<th className="p-3 text-center">عملیات مدیریت</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/60">
{filteredItems.map((item) => (
<tr key={item.id} className="hover:bg-[#0e1933] transition-colors">
<td className="p-3 font-bold text-white max-w-xs">{item.title}</td>
<td className="p-3">
<span
className="px-2 py-0.5 rounded-full text-[10px] font-bold"
style={{
backgroundColor: `${getCategoryColor(item.category)}18`,
color: getCategoryColor(item.category)
}}
>
{getCategoryLabelFa(item.category)}
</span>
</td>
<td className="p-3 text-slate-300">{item.horizon}</td>
<td className="p-3 font-bold text-teal-400">{toPersianDigits(item.impactScore)}/۵</td>
<td className="p-3 text-slate-300">{item.geographicScope}</td>
<td className="p-3">
<span className="badge badge-gray">{item.status}</span>
</td>
<td className="p-3 text-center">
<div className="flex items-center justify-center gap-1">
<button
onClick={() => handleEdit(item)}
className="p-1.5 text-sky-400 hover:text-white bg-[#0e1c3a] hover:bg-sky-900/40 rounded-lg transition-colors"
title="ویرایش"
>
<Edit2 size={13} />
</button>
<button
onClick={() => handleDelete(item.id)}
className="p-1.5 text-rose-400 hover:text-white bg-[#1a0e1c] hover:bg-rose-950/40 rounded-lg transition-colors"
title="حذف"
>
<Trash2 size={13} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Tab 2: 1415 Dimensions Table */}
{activeTab === 'dimensions' && (
<div className="intel-card overflow-x-auto p-0">
<table className="w-full text-right text-xs border-collapse">
<thead>
<tr className="bg-[#091122] text-slate-400 border-b border-slate-800">
<th className="p-3">کلانویژگی دانشگاه ۱۴۱۵</th>
<th className="p-3">نمره وضع موجود (۱-۱۰)</th>
<th className="p-3">نمره هدف ۱۴۱۵ (۱-۱۰)</th>
<th className="p-3">شکاف راهبردی</th>
<th className="p-3">درصد آمادگی</th>
<th className="p-3">تعداد شواهد</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/60">
{dimensions.map((dim) => (
<tr key={dim.id} className="hover:bg-[#0e1933] transition-colors">
<td className="p-3 font-bold text-white">{dim.title}</td>
<td className="p-3 text-amber-400 font-bold font-mono">{toPersianDigits(dim.currentScore)}</td>
<td className="p-3 text-teal-400 font-bold font-mono">{toPersianDigits(dim.targetScore)}</td>
<td className="p-3 text-rose-400 font-extrabold font-mono">{toPersianDigits(dim.gap)}</td>
<td className="p-3 text-teal-300 font-bold font-mono">{toPersianDigits(dim.readinessPercentage)}٪</td>
<td className="p-3 text-slate-400">{toPersianDigits(dim.supportingEvidence.length)} مورد</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Tab 3: Jobs & Skills Table */}
{activeTab === 'jobs' && (
<div className="intel-card overflow-x-auto p-0">
<table className="w-full text-right text-xs border-collapse">
<thead>
<tr className="bg-[#091122] text-slate-400 border-b border-slate-800">
<th className="p-3">عنوان شغل</th>
<th className="p-3">نوع تحول</th>
<th className="p-3">نرخ رشد</th>
<th className="p-3">خطر اتوماسیون</th>
<th className="p-3">دانشکده مرتبط در دانشگاه اصفهان</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/60">
{jobs.map((job) => (
<tr key={job.id} className="hover:bg-[#0e1933] transition-colors">
<td className="p-3 font-bold text-white">{job.title}</td>
<td className="p-3">
<span className={`badge ${job.type === 'growing' ? 'badge-teal' : 'badge-rose'}`}>
{job.type === 'growing' ? 'رو به رشد' : 'در معرض تغییر'}
</span>
</td>
<td className="p-3 font-mono text-teal-400 font-bold">+{toPersianDigits(job.growthRatePercent)}٪</td>
<td className="p-3 font-mono text-rose-400">{toPersianDigits(job.automationRiskPercent)}٪</td>
<td className="p-3 text-slate-300">{job.discipline}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<EntityEditorModal
isOpen={isEditorOpen}
onClose={() => setIsEditorOpen(false)}
onSave={handleSaveItem}
initialItem={editingItem}
/>
</div>
);
};

503
src/pages/Dashboard.tsx Normal file
View File

@ -0,0 +1,503 @@
import React, { useState } from 'react';
import { IntelligenceItem, University1415Dimension, Category, Horizon, UserSession } from '../types/futures';
import { dataService } from '../services/dataService';
import { FuturesRadar } from '../components/visual/FuturesRadar';
import { PersonaBanner } from '../components/layout/PersonaBanner';
import { toPersianDigits } from '../utils/persianNumbers';
import {
Sparkles,
TrendingUp,
Cpu,
BookOpen,
Briefcase,
Target,
ArrowUpRight,
ArrowLeft,
AlertTriangle,
Lightbulb,
CheckCircle2,
Calendar,
Layers,
Triangle,
GitFork,
FileText
} from 'lucide-react';
interface DashboardProps {
currentSession: UserSession | null;
items: IntelligenceItem[];
dimensions: University1415Dimension[];
onSelectItem: (item: IntelligenceItem) => void;
onSelectDimension: (dim: University1415Dimension) => void;
onNavigate: (route: string) => void;
}
export const Dashboard: React.FC<DashboardProps> = ({
currentSession,
items,
dimensions,
onSelectItem,
onSelectDimension,
onNavigate
}) => {
const [radarCategory, setRadarCategory] = useState<Category | 'all'>('all');
const [radarHorizon, setRadarHorizon] = useState<Horizon | 'all'>('all');
const kpis = dataService.getExecutiveKpis();
// Top critical items
const criticalItems = items
.filter((i) => i.status === 'critical' || i.impactScore === 5)
.slice(0, 4);
// Top gap dimensions
const topGaps = [...dimensions].sort((a, b) => b.gap - a.gap).slice(0, 3);
// Average readiness
const avgReadiness = Math.round(
dimensions.reduce((acc, d) => acc + d.readinessPercentage, 0) / dimensions.length
);
return (
<div className="flex flex-col gap-6 animate-fadeIn">
{/* Persona Context Reminder */}
<PersonaBanner currentSession={currentSession} />
{/* Top Welcome & Foresight Mission Bar */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-1">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="text-xl font-bold">👋</span>
<h1 className="text-2xl lg:text-3xl font-black text-slate-900 tracking-tight">
رادار آینده دانشگاه اصفهان
</h1>
<span className="badge badge-teal font-bold">سال تحصیلی ۱۴۰۳-۱۴۰۴</span>
</div>
<p className="text-slate-500 text-xs mt-0.5">
دیدهبانی مستمر پیشرانها، فناوریها، سیگنالهای ضعیف و سنجش آمادگی دانشگاه در افق ۱۴۱۵
</p>
</div>
<div className="flex items-center gap-2.5 self-start md:self-auto">
<button
onClick={() => onNavigate('/reports')}
className="btn btn-secondary text-xs flex items-center gap-2 py-2 px-4 shadow-sm"
>
<FileText size={14} className="text-slate-500" />
<span>بولتن مدیریتی هیئت رئیسه</span>
</button>
<button
onClick={() => onNavigate('/gap-analysis')}
className="btn btn-primary text-xs flex items-center gap-2 py-2 px-4 shadow-sm"
>
<span>تحلیل جامع شکاف ۱۴۱۵</span>
<ArrowLeft size={13} />
</button>
</div>
</div>
{/* 6 Bento Pastel KPI Cards (Inspired by Images 1, 2, 3, 5) */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3.5">
{/* KPI 1: Active Signals (Blush Pink Card) */}
<div
onClick={() => onNavigate('/radar?category=weak_signal')}
className="intel-card interactive bento-card-pink p-4 flex flex-col justify-between"
>
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-rose-700">سیگنالهای آینده</span>
<div className="circle-action-btn text-rose-600">
<ArrowUpRight size={17} />
</div>
</div>
<div className="my-2">
<span className="kpi-number text-3xl font-black text-slate-900">
{toPersianDigits(kpis.totalSignals)}
</span>
</div>
<div className="flex items-center justify-between text-[11px] text-rose-800 font-semibold pt-1 border-t border-rose-200/60">
<span>۹ سیگنال فعال</span>
<Sparkles size={13} className="text-rose-500" />
</div>
</div>
{/* KPI 2: Emerging Trends (Soft Sky Blue Card) */}
<div
onClick={() => onNavigate('/radar?category=trend')}
className="intel-card interactive bento-card-blue p-4 flex flex-col justify-between"
>
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-sky-700">روندهای نوظهور</span>
<div className="circle-action-btn text-sky-600">
<ArrowUpRight size={17} />
</div>
</div>
<div className="my-2">
<span className="kpi-number text-3xl font-black text-slate-900">
{toPersianDigits(kpis.emergingTrends)}
</span>
</div>
<div className="flex items-center justify-between text-[11px] text-sky-800 font-semibold pt-1 border-t border-sky-200/60">
<span>رانندههای پداگوژی</span>
<TrendingUp size={13} className="text-sky-500" />
</div>
</div>
{/* KPI 3: Key Technologies (Soft Mint Card) */}
<div
onClick={() => onNavigate('/radar?category=technology')}
className="intel-card interactive bento-card-mint p-4 flex flex-col justify-between"
>
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-emerald-700">فناوریهای کلیدی</span>
<div className="circle-action-btn text-emerald-600">
<ArrowUpRight size={17} />
</div>
</div>
<div className="my-2">
<span className="kpi-number text-3xl font-black text-slate-900">
{toPersianDigits(kpis.criticalTech)}
</span>
</div>
<div className="flex items-center justify-between text-[11px] text-emerald-800 font-semibold pt-1 border-t border-emerald-200/60">
<span>اثرگذاری اخلالگر</span>
<Cpu size={13} className="text-emerald-500" />
</div>
</div>
{/* KPI 4: Policy Shifts (Soft Buttery Yellow Card) */}
<div
onClick={() => onNavigate('/radar?category=policy')}
className="intel-card interactive bento-card-yellow p-4 flex flex-col justify-between"
>
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-amber-800">تحولات قانونی</span>
<div className="circle-action-btn text-amber-600">
<ArrowUpRight size={17} />
</div>
</div>
<div className="my-2">
<span className="kpi-number text-3xl font-black text-slate-900">
{toPersianDigits(kpis.policyChanges)}
</span>
</div>
<div className="flex items-center justify-between text-[11px] text-amber-900 font-semibold pt-1 border-t border-amber-200/60">
<span>آییننامهها و مالیات</span>
<BookOpen size={13} className="text-amber-600" />
</div>
</div>
{/* KPI 5: Labor Market Disruption (Soft Lilac Purple Card) */}
<div
onClick={() => onNavigate('/labor-market')}
className="intel-card interactive bento-card-purple p-4 flex flex-col justify-between"
>
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-purple-700">تغییرات بازار کار</span>
<div className="circle-action-btn text-purple-600">
<ArrowUpRight size={17} />
</div>
</div>
<div className="my-2">
<span className="kpi-number text-3xl font-black text-slate-900">
{toPersianDigits(kpis.highImpactLabor)}
</span>
</div>
<div className="flex items-center justify-between text-[11px] text-purple-800 font-semibold pt-1 border-t border-purple-200/60">
<span>شغل در دگرگونی</span>
<Briefcase size={13} className="text-purple-500" />
</div>
</div>
{/* KPI 6: Average 1415 Readiness (Crisp White Card with Mini Arc/Bar) */}
<div
onClick={() => onNavigate('/gap-analysis')}
className="intel-card interactive p-4 flex flex-col justify-between border-teal-200"
>
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-teal-800">آمادگی افق ۱۴۱۵</span>
<div className="circle-action-btn text-teal-600">
<ArrowUpRight size={17} />
</div>
</div>
<div className="my-2">
<span className="kpi-number text-3xl font-black text-teal-600">
{toPersianDigits(avgReadiness)}٪
</span>
</div>
<div className="w-full bg-slate-100 h-2 rounded-full overflow-hidden">
<div className="bg-teal-500 h-full rounded-full" style={{ width: `${avgReadiness}%` }}></div>
</div>
<span className="text-[10.5px] text-slate-400 font-medium mt-1">فاصله تا وضع مطلوب: ۵۰٪</span>
</div>
</div>
{/* Main Centerpiece: Split Layout (Futures Radar on Left + Readiness & Alerts on Right) */}
<div className="grid grid-cols-1 xl:grid-cols-12 gap-6 items-start">
{/* Main Left Column: Interactive Futures Radar Bento Card (7 Cols) */}
<div className="xl:col-span-7 intel-card flex flex-col p-6">
<div className="flex flex-wrap items-center justify-between gap-3 pb-4 border-b border-slate-100">
<div>
<h3 className="text-slate-900 font-black text-base flex items-center gap-2">
<Target size={18} className="text-teal-600" />
<span>رادار تعاملی آیندهپژوهی آموزش عالی</span>
</h3>
<p className="text-slate-500 text-xs mt-0.5">
موقعیتیابی بر اساس ۴ افق زمانی، شدت اثر و دستهبندی موضوعی
</p>
</div>
{/* Pill Filter Switcher (Inspired by Image 4 Top Nav Pills) */}
<div className="flex items-center bg-slate-100 p-1 rounded-full text-xs">
<button
onClick={() => setRadarCategory('all')}
className={`px-3 py-1 rounded-full font-bold transition-all ${
radarCategory === 'all'
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
همه ({toPersianDigits(items.length)})
</button>
<button
onClick={() => setRadarCategory('trend')}
className={`px-3 py-1 rounded-full font-bold transition-all ${
radarCategory === 'trend'
? 'bg-sky-600 text-white shadow-sm'
: 'text-sky-700 hover:text-sky-900'
}`}
>
روندها
</button>
<button
onClick={() => setRadarCategory('technology')}
className={`px-3 py-1 rounded-full font-bold transition-all ${
radarCategory === 'technology'
? 'bg-teal-600 text-white shadow-sm'
: 'text-teal-700 hover:text-teal-900'
}`}
>
فناوریها
</button>
<button
onClick={() => setRadarCategory('policy')}
className={`px-3 py-1 rounded-full font-bold transition-all ${
radarCategory === 'policy'
? 'bg-amber-600 text-white shadow-sm'
: 'text-amber-700 hover:text-amber-900'
}`}
>
سیاستها
</button>
<button
onClick={() => setRadarCategory('weak_signal')}
className={`px-3 py-1 rounded-full font-bold transition-all ${
radarCategory === 'weak_signal'
? 'bg-rose-600 text-white shadow-sm'
: 'text-rose-700 hover:text-rose-900'
}`}
>
سیگنالها
</button>
</div>
</div>
{/* Radar Visualization Canvas */}
<div className="py-2 flex items-center justify-center">
<FuturesRadar
items={items}
selectedCategory={radarCategory}
selectedHorizon={radarHorizon}
onSelectItem={onSelectItem}
height={520}
/>
</div>
{/* Radar Action Footer */}
<div className="pt-3 border-t border-slate-100 flex items-center justify-between text-xs text-slate-500">
<span>برای بررسی پرونده تحلیلی، روی هر نقطه کلیک کنید.</span>
<button
onClick={() => onNavigate('/radar')}
className="text-teal-600 font-bold hover:underline flex items-center gap-1"
>
نمایش تمامصفحه رادار با ماتریس تحلیلی
<ArrowLeft size={13} />
</button>
</div>
</div>
{/* Right Column: Readiness Arc Meter & Strategic Schedule (5 Cols) */}
<div className="xl:col-span-5 flex flex-col gap-5">
{/* Card 1: 1415 Readiness Arc & Donut Meter (Inspired by Image 2 TVL & Image 3 Customer Segments) */}
<div className="intel-card p-5 flex flex-col gap-4">
<div className="flex items-center justify-between pb-3 border-b border-slate-100">
<div>
<h4 className="text-slate-900 font-black text-sm flex items-center gap-2">
<Target size={17} className="text-teal-600" />
<span>شاخص بلوغ و آمادگی دانشگاه در سال ۱۴۱۵</span>
</h4>
<p className="text-slate-500 text-[11px] mt-0.5">ارزیابی ۱۰ کلانویژگی مصوب چشمانداز</p>
</div>
<span className="badge badge-teal font-bold">وضع موجود</span>
</div>
{/* Arc Progress Visualization */}
<div className="flex items-center justify-between gap-4 py-1">
<div className="relative w-36 h-36 shrink-0 flex items-center justify-center">
{/* SVG Semi-Circle Arc Meter */}
<svg viewBox="0 0 100 100" className="w-full h-full transform -rotate-90">
<circle
cx="50"
cy="50"
r="40"
fill="transparent"
stroke="#f1f5f9"
strokeWidth="10"
/>
<circle
cx="50"
cy="50"
r="40"
fill="transparent"
stroke="#00b894"
strokeWidth="10"
strokeDasharray={`${(avgReadiness / 100) * 251.2} 251.2`}
strokeLinecap="round"
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center text-center">
<span className="kpi-number text-2xl font-black text-slate-900">{toPersianDigits(avgReadiness)}٪</span>
<span className="text-[10px] text-slate-400 font-bold">کل آمادگی</span>
</div>
</div>
{/* Top Priority Gaps breakdown */}
<div className="flex-1 flex flex-col gap-2">
<span className="text-xs font-bold text-slate-700">بزرگترین شکافهای نیازمند مداخله:</span>
{topGaps.map((dim) => (
<div
key={dim.id}
onClick={() => onSelectDimension(dim)}
className="p-2 bg-slate-50 hover:bg-slate-100 rounded-xl cursor-pointer transition-colors flex items-center justify-between text-xs border border-slate-200/70"
>
<span className="font-bold text-slate-800">{dim.title}</span>
<span className="text-rose-600 font-black">
شکاف: {toPersianDigits(dim.gap)}
</span>
</div>
))}
</div>
</div>
<button
onClick={() => onNavigate('/gap-analysis')}
className="w-full py-2 bg-slate-100 hover:bg-slate-200/80 text-slate-800 font-bold rounded-xl text-xs flex items-center justify-center gap-1.5 transition-colors"
>
<span>مشاهده نمودار عنکبوتی ۱۰ بعدی و جزئیات</span>
<ArrowLeft size={13} />
</button>
</div>
{/* Card 2: Immediate Foresight Alerts List */}
<div className="intel-card p-5 flex flex-col gap-3.5">
<div className="flex items-center justify-between pb-3 border-b border-slate-100">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-rose-50 border border-rose-200 text-rose-600 flex items-center justify-center">
<AlertTriangle size={16} />
</div>
<div>
<h4 className="text-slate-900 font-extrabold text-sm">پیشرانهای با فوریت بحرانی (افق اکنون)</h4>
<span className="text-slate-400 text-[11px]">نیازمند تصمیم در شورای دانشگاه</span>
</div>
</div>
<span className="badge badge-rose">فوری</span>
</div>
{/* List of Critical Items as Pastel Pill Rows */}
<div className="flex flex-col gap-2.5">
{criticalItems.map((item) => (
<div
key={item.id}
onClick={() => onSelectItem(item)}
className="p-3 bg-slate-50 hover:bg-white hover:border-teal-300 border border-slate-200/80 rounded-2xl cursor-pointer transition-all flex flex-col gap-1 shadow-sm"
>
<div className="flex items-center justify-between">
<span className="font-bold text-slate-900 text-xs leading-snug">{item.title}</span>
<span className="text-[10px] bg-rose-100 text-rose-700 font-bold px-2 py-0.5 rounded-full shrink-0">
تأثیر: {toPersianDigits(item.impactScore)}/۵
</span>
</div>
<p className="text-slate-500 text-[11px] line-clamp-1 mt-0.5 leading-relaxed">
{item.whyItMatters}
</p>
</div>
))}
</div>
</div>
</div>
</div>
{/* Bottom Bento Row: Futures Triangle + Strategic Impact Value Chain */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Futures Triangle Teaser */}
<div
onClick={() => onNavigate('/futures-triangle')}
className="intel-card interactive bento-card-purple p-6 flex flex-col justify-between"
>
<div className="flex items-center justify-between pb-3 border-b border-purple-200/60">
<div className="flex items-center gap-2.5">
<div className="w-10 h-10 rounded-2xl bg-white text-purple-600 flex items-center justify-center shadow-sm">
<Triangle size={20} />
</div>
<div>
<span className="text-xs font-bold text-purple-700 block mb-0.5">پویایی تعادل نیروها</span>
<h4 className="text-slate-900 font-black text-base">مثلث هوشمندی آینده دانشگاه اصفهان</h4>
</div>
</div>
<div className="circle-action-btn text-purple-700">
<ArrowUpRight size={18} />
</div>
</div>
<p className="text-slate-700 text-xs leading-relaxed my-3">
سنجش برآیند <strong>کشش آینده</strong> (دانشگاه هوشمند و مادامالعمر)، <strong>فشارهای حال</strong> (شتاب هوش مصنوعی و بحران آب اصفهان) و <strong>وزن گذشته</strong> (دیوانسالاری کهنه و ساختار سنتی)
</p>
<div className="flex items-center justify-between pt-2 border-t border-purple-200/60 text-xs font-bold text-purple-900">
<span>کاوش تعاملی ۱۵ نیروی کلیدی در مثلث آینده</span>
<ArrowLeft size={13} />
</div>
</div>
{/* Strategic Disruption Chain Teaser */}
<div
onClick={() => onNavigate('/network')}
className="intel-card interactive bento-card-mint p-6 flex flex-col justify-between"
>
<div className="flex items-center justify-between pb-3 border-b border-emerald-200/60">
<div className="flex items-center gap-2.5">
<div className="w-10 h-10 rounded-2xl bg-white text-emerald-600 flex items-center justify-center shadow-sm">
<GitFork size={20} />
</div>
<div>
<span className="text-xs font-bold text-teal-700 block mb-0.5">زنجیره ارزش و سناریوسازی</span>
<h4 className="text-slate-900 font-black text-base">شبکه روابط راهبردی: از سیگنال بیرونی تا شکاف قابلیت</h4>
</div>
</div>
<div className="circle-action-btn text-emerald-700">
<ArrowUpRight size={18} />
</div>
</div>
<p className="text-slate-700 text-xs leading-relaxed my-3">
ردگیری زنجیره اخلال: فناوری نوین دگرگونی آموزش مهارتهای موردنیاز بازار کار بازآفرینی مشاغل بعد دانشگاه اصفهان ۱۴۱۵ شکاف عملکردی
</p>
<div className="flex items-center justify-between pt-2 border-t border-emerald-200/60 text-xs font-bold text-emerald-900">
<span>مشاهده گراف شبکه و روابط متقابل</span>
<ArrowLeft size={13} />
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,72 @@
import React from 'react';
import { FuturesForce } from '../types/futures';
import { TriangleView } from '../components/visual/TriangleView';
import { toPersianDigits } from '../utils/persianNumbers';
import { Sparkles, TrendingUp, ShieldAlert } from 'lucide-react';
interface FuturesTrianglePageProps {
forces: FuturesForce[];
}
export const FuturesTrianglePage: React.FC<FuturesTrianglePageProps> = ({ forces }) => {
return (
<div className="flex flex-col gap-6 animate-fadeIn">
{/* Clean Executive Header (Zero AI Slop) */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-2 border-b border-slate-200">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="text-slate-400 text-xs font-bold">برنامهریزی راهبردی / تحلیل تعادل نیروها</span>
</div>
<h1 className="text-2xl lg:text-3xl font-black text-slate-900 tracking-tight">
مثلث هوشمندی آینده دانشگاه اصفهان
</h1>
<p className="text-slate-500 text-xs mt-1">
سنجش برآیند کشش آینده، فشارهای جاری زمان حال و وزن لنگرهای ساختاری گذشته
</p>
</div>
<div className="flex items-center gap-2">
<span className="text-xs bg-slate-100 border border-slate-200 text-slate-800 px-3.5 py-1.5 rounded-full font-bold shadow-sm">
{toPersianDigits(forces.length)} نیروی کلیدی در تعامل
</span>
</div>
</div>
{/* 3 Executive Pastel Bento Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
<div className="intel-card bento-card-purple p-4 flex flex-col justify-between">
<div className="flex items-center gap-2 text-purple-950 font-black mb-1">
<Sparkles size={16} className="text-purple-600" />
<span className="text-sm">کشش آینده ۵ نیرو</span>
</div>
<p className="text-slate-600 text-[11.5px] leading-relaxed">
تصویر مطلوب، دانشگاههای نسل پنجم، آموزش هوشمند و یادگیری مادامالعمر به عنوان نیروهای جاذب.
</p>
</div>
<div className="intel-card bento-card-blue p-4 flex flex-col justify-between">
<div className="flex items-center gap-2 text-sky-950 font-black mb-1">
<TrendingUp size={16} className="text-sky-600" />
<span className="text-sm">فشارهای زمان حال ۵ نیرو</span>
</div>
<p className="text-slate-600 text-[11.5px] leading-relaxed">
شتاب هوش مصنوعی، انقباض هرم جمعیتی، بحران منابع و آب اصفهان و کسری بودجه عمومی.
</p>
</div>
<div className="intel-card bento-card-pink p-4 flex flex-col justify-between">
<div className="flex items-center gap-2 text-rose-950 font-black mb-1">
<ShieldAlert size={16} className="text-rose-600" />
<span className="text-sm">وزن گذشته ۵ نیرو</span>
</div>
<p className="text-slate-600 text-[11.5px] leading-relaxed">
لختی دیوانسالاری، آییننامههای سنتی ارتقا، ابنیه فرسوده و مقاومت فرهنگی در برابر دگرگونی.
</p>
</div>
</div>
{/* Sleek Geometric Triangle Visualizer */}
<TriangleView forces={forces} />
</div>
);
};

View File

@ -0,0 +1,275 @@
import React, { useState } from 'react';
import { University1415Dimension } from '../types/futures';
import { SpiderChart } from '../components/visual/SpiderChart';
import { toPersianDigits } from '../utils/persianNumbers';
import {
Target,
ArrowLeft,
AlertTriangle,
Award,
TrendingUp,
Lightbulb,
ArrowUpRight
} from 'lucide-react';
interface GapAnalysisPageProps {
dimensions: University1415Dimension[];
onSelectDimension: (dim: University1415Dimension) => void;
}
export const GapAnalysisPage: React.FC<GapAnalysisPageProps> = ({
dimensions,
onSelectDimension
}) => {
const [activeTab, setActiveTab] = useState<'spider' | 'breakdown'>('spider');
// Calculations
const sortedByGap = [...dimensions].sort((a, b) => b.gap - a.gap);
const sortedByScore = [...dimensions].sort((a, b) => b.currentScore - a.currentScore);
const largestGaps = sortedByGap.slice(0, 4);
const strongestDims = sortedByScore.slice(0, 3);
const avgReadiness = Math.round(
dimensions.reduce((acc, d) => acc + d.readinessPercentage, 0) / dimensions.length
);
return (
<div className="flex flex-col gap-6 animate-fadeIn">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-2 border-b border-slate-200">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="badge badge-rose">سنجش بلوغ سازمانی</span>
<span className="text-slate-400 text-xs font-semibold">ارزیابی شکاف آمادگی افق ۱۴۱۵</span>
</div>
<h1 className="page-title text-2xl lg:text-3xl text-slate-900">
تحلیل جامع شکاف راهبردی (Gap Analysis)
</h1>
<p className="page-subtitle text-slate-500 text-xs mt-1">
مقایسه چندبُعدی وضعیت موجود دانشگاه اصفهان با اهداف آرمانی دانشگاه آینده در سال ۱۴۱۵
</p>
</div>
{/* Pill Tab Switcher */}
<div className="flex items-center bg-slate-100 border border-slate-200 p-1 rounded-full text-xs shadow-sm">
<button
onClick={() => setActiveTab('spider')}
className={`px-4 py-1.5 rounded-full transition-all font-bold ${
activeTab === 'spider'
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
نمودار عنکبوتی ۱۰ بعدی
</button>
<button
onClick={() => setActiveTab('breakdown')}
className={`px-4 py-1.5 rounded-full transition-all font-bold ${
activeTab === 'breakdown'
? 'bg-slate-900 text-white shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
ماتریس اولویتبندی مداخله
</button>
</div>
</div>
{/* KPI Cards Row (Bento Pastel Cards matching Image 1 & 3) */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Overall Readiness (Mint) */}
<div className="intel-card bento-card-mint p-5 flex flex-col justify-between">
<div className="flex items-center justify-between text-xs text-emerald-800 font-bold mb-2">
<span>شاخص کل آمادگی دانشگاه</span>
<Target size={17} className="text-emerald-600" />
</div>
<div className="text-3xl font-black text-slate-900">
{toPersianDigits(avgReadiness)}٪
</div>
<div className="w-full h-2 bg-emerald-200/80 rounded-full mt-2 overflow-hidden">
<div
className="h-full bg-emerald-600 rounded-full"
style={{ width: `${avgReadiness}%` }}
></div>
</div>
<span className="text-[11px] text-emerald-900 font-semibold mt-1">فاصله تا وضع مطلوب: ۵۴٪</span>
</div>
{/* Largest Gap (Pink) */}
<div className="intel-card bento-card-pink p-5 flex flex-col justify-between">
<div className="flex items-center justify-between text-xs text-rose-800 font-bold mb-1">
<span>بزرگترین شکاف عملکردی</span>
<AlertTriangle size={17} className="text-rose-600" />
</div>
<div className="text-sm font-extrabold text-slate-900 mt-1">
{largestGaps[0].title}
</div>
<div className="text-xl font-black text-rose-700 mt-1">
شکاف: {toPersianDigits(largestGaps[0].gap)} نمره
</div>
<span className="text-[11px] text-rose-800 font-semibold">
آمادگی: {toPersianDigits(largestGaps[0].readinessPercentage)}٪
</span>
</div>
{/* Second Largest Gap (Yellow) */}
<div className="intel-card bento-card-yellow p-5 flex flex-col justify-between">
<div className="flex items-center justify-between text-xs text-amber-800 font-bold mb-1">
<span>دومین اولویت مداخله فوری</span>
<TrendingUp size={17} className="text-amber-600" />
</div>
<div className="text-sm font-extrabold text-slate-900 mt-1">
{largestGaps[1].title}
</div>
<div className="text-xl font-black text-amber-800 mt-1">
شکاف: {toPersianDigits(largestGaps[1].gap)} نمره
</div>
<span className="text-[11px] text-amber-900 font-semibold">
آمادگی: {toPersianDigits(largestGaps[1].readinessPercentage)}٪
</span>
</div>
{/* Strongest Dimension (Purple) */}
<div className="intel-card bento-card-purple p-5 flex flex-col justify-between">
<div className="flex items-center justify-between text-xs text-purple-800 font-bold mb-1">
<span>نقطه قوت و مزیت کنونی</span>
<Award size={17} className="text-purple-600" />
</div>
<div className="text-sm font-extrabold text-slate-900 mt-1">
{strongestDims[0].title}
</div>
<div className="text-xl font-black text-purple-900 mt-1">
نمره: {toPersianDigits(strongestDims[0].currentScore)} از ۱۰
</div>
<span className="text-[11px] text-purple-800 font-semibold">
آمادگی: {toPersianDigits(strongestDims[0].readinessPercentage)}٪
</span>
</div>
</div>
{/* Main Spider Chart Visualization Container */}
{activeTab === 'spider' && (
<div className="grid grid-cols-1 xl:grid-cols-12 gap-6 items-start">
<div className="xl:col-span-8 intel-card bg-white border border-slate-200 rounded-3xl p-6 shadow-sm flex flex-col items-center justify-center">
<SpiderChart
dimensions={dimensions}
onSelectDimension={onSelectDimension}
height={580}
/>
</div>
{/* Right Column: Strategic Gap Diagnosis & Recommendations */}
<div className="xl:col-span-4 flex flex-col gap-4">
<div className="intel-card bg-white border border-slate-200 rounded-3xl p-5 shadow-sm flex flex-col gap-3">
<h3 className="text-slate-900 font-extrabold text-sm border-b border-slate-100 pb-2.5">
رتبهبندی شکافهای دهگانه:
</h3>
<div className="flex flex-col gap-2">
{sortedByGap.map((dim, idx) => (
<div
key={dim.id}
onClick={() => onSelectDimension(dim)}
className="bg-slate-50 hover:bg-slate-100 border border-slate-200 p-2.5 rounded-2xl cursor-pointer transition-all flex items-center justify-between text-xs"
>
<div className="flex items-center gap-2.5">
<span className="w-5 h-5 rounded-full bg-slate-200 text-slate-700 flex items-center justify-center text-[10px] font-bold">
{toPersianDigits(idx + 1)}
</span>
<span className="font-extrabold text-slate-800">{dim.shortTag}</span>
</div>
<div className="flex items-center gap-3">
<span className="text-amber-700 font-bold">
{toPersianDigits(dim.currentScore)}/۱۰
</span>
<span className="text-rose-700 font-bold bg-rose-50 border border-rose-200 px-2 py-0.5 rounded-full text-[11px]">
شکاف: {toPersianDigits(dim.gap)}
</span>
</div>
</div>
))}
</div>
</div>
<div className="intel-card bento-card-mint p-5 rounded-3xl flex flex-col gap-2">
<div className="flex items-center gap-2 text-emerald-800 font-bold text-xs">
<Lightbulb size={16} className="text-emerald-600" />
<span>فرمان اجرایی اولویتدار:</span>
</div>
<p className="text-slate-700 text-xs leading-relaxed">
برای کاهش شکاف ۵.۳ نمرهای در بعد هوشمصنوعیبنیان، نیاز به خرید ۳۰ سرور پردازشی GPU بومی و تغییر نظام ارزیابی امتحانات در ۳۰ گروه آموزشی تا بهمن ۱۴۰۳ است.
</p>
</div>
</div>
</div>
)}
{/* Tab 2: Detailed Matrix Table Breakdown */}
{activeTab === 'breakdown' && (
<div className="intel-card overflow-x-auto p-0 bg-white border border-slate-200 rounded-3xl shadow-sm animate-fadeIn">
<table className="w-full text-right text-xs border-collapse">
<thead>
<tr className="bg-slate-50 text-slate-500 border-b border-slate-200">
<th className="p-4 font-bold">کلانویژگی دانشگاه ۱۴۱۵</th>
<th className="p-4 font-bold">وضع موجود (۱-۱۰)</th>
<th className="p-4 font-bold">هدف ۱۴۱۵ (۱-۱۰)</th>
<th className="p-4 font-bold">میزان شکاف</th>
<th className="p-4 font-bold">درصد آمادگی</th>
<th className="p-4 font-bold">اقدام راهبردی اولویتدار</th>
<th className="p-4 font-bold">اقدام</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{dimensions.map((dim) => (
<tr
key={dim.id}
onClick={() => onSelectDimension(dim)}
className="hover:bg-slate-50 cursor-pointer transition-colors"
>
<td className="p-4 font-extrabold text-slate-900">
<span className="flex items-center gap-2">
<span
className="w-2.5 h-2.5 rounded-full"
style={{ backgroundColor: dim.pillarColor }}
></span>
{dim.title}
</span>
</td>
<td className="p-4 text-amber-700 font-bold">{toPersianDigits(dim.currentScore)}</td>
<td className="p-4 text-teal-700 font-bold">{toPersianDigits(dim.targetScore)}</td>
<td className="p-4 text-rose-700 font-extrabold">
{toPersianDigits(dim.gap)}
</td>
<td className="p-4">
<div className="flex items-center gap-2">
<div className="w-16 h-1.5 bg-slate-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full"
style={{
width: `${dim.readinessPercentage}%`,
backgroundColor: dim.pillarColor
}}
></div>
</div>
<span className="text-slate-700 font-bold">{toPersianDigits(dim.readinessPercentage)}٪</span>
</div>
</td>
<td className="p-4 text-slate-600 max-w-sm line-clamp-1">
{dim.strategicRecommendations[0]}
</td>
<td className="p-4">
<button className="text-teal-600 font-bold hover:underline">
جزئیات بعد
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,375 @@
import React from 'react';
import {
IntelligenceItem,
University1415Dimension,
JobEntity,
SkillEntity
} from '../types/futures';
import { ImpactGauges } from '../components/visual/ImpactGauges';
import {
toPersianDigits,
getCategoryLabelFa,
getCategoryColor,
getHorizonLabelFa
} from '../utils/persianNumbers';
import {
ArrowRight,
Clock,
Zap,
Globe,
HelpCircle,
FileText,
Building,
AlertOctagon,
ShieldCheck,
CheckCircle2,
AlertTriangle,
Lightbulb,
Target,
Briefcase,
Brain,
Layers,
ArrowUpRight
} from 'lucide-react';
interface ItemDetailPageProps {
item: IntelligenceItem;
allItems: IntelligenceItem[];
dimensions: University1415Dimension[];
jobs: JobEntity[];
skills: SkillEntity[];
onBack: () => void;
onSelectItem: (item: IntelligenceItem) => void;
onSelectDimension: (dim: University1415Dimension) => void;
}
export const ItemDetailPage: React.FC<ItemDetailPageProps> = ({
item,
allItems,
dimensions,
jobs,
skills,
onBack,
onSelectItem,
onSelectDimension
}) => {
const catColor = getCategoryColor(item.category);
// Trace related items
const relatedItems = allItems.filter((i) => item.relatedItemIds?.includes(i.id));
const relatedDimensions = dimensions.filter((d) => item.relatedDimensionIds?.includes(d.id));
const relatedJobs = jobs.filter((j) => item.relatedJobIds?.includes(j.id));
const relatedSkills = skills.filter((s) => item.relatedSkillIds?.includes(s.id));
return (
<div className="flex flex-col gap-6 animate-fadeIn pb-12">
{/* Back Button & Top Navigation */}
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
<button
onClick={onBack}
className="btn btn-secondary text-xs flex items-center gap-2 py-2 px-4 shadow-sm"
>
<ArrowRight size={15} />
<span>بازگشت به رادار هوشمندی</span>
</button>
<div className="flex items-center gap-2">
<span
className="text-xs font-bold px-3 py-1 rounded-full"
style={{
backgroundColor: `${catColor}15`,
color: catColor
}}
>
{getCategoryLabelFa(item.category)}
</span>
<span className="badge badge-gray font-mono">شناسه: {item.id}</span>
</div>
</div>
{/* 1. OVERVIEW SECTION (Bento Card) */}
<div
className="intel-card bg-white border border-slate-200 rounded-3xl p-6 lg:p-8 shadow-sm"
style={{ borderRight: `5px solid ${catColor}` }}
>
<div className="flex flex-wrap items-center gap-4 text-xs text-slate-500 font-semibold mb-3">
<span className="flex items-center gap-1.5 text-teal-700 font-bold">
<Zap size={14} className="text-teal-600" />
شدت اثر کلی: {toPersianDigits(item.impactScore)} از ۵
</span>
<span></span>
<span className="flex items-center gap-1.5 text-slate-700">
<Clock size={14} className="text-slate-400" />
افق زمانی: {getHorizonLabelFa(item.horizon)}
</span>
<span></span>
<span className="flex items-center gap-1.5 text-slate-700">
<Globe size={14} className="text-slate-400" />
دامنه جغرافیایی: {item.geographicScope}
</span>
<span></span>
<span className="flex items-center gap-1.5 text-amber-700">
<HelpCircle size={14} className="text-amber-500" />
عدم قطعیت: {toPersianDigits(item.uncertaintyScore)} از ۵
</span>
</div>
<h1 className="text-slate-900 font-black text-xl lg:text-2xl leading-tight mb-3">
{item.title}
</h1>
<div className="bg-slate-50 border border-slate-200 p-4 rounded-2xl text-xs text-slate-700 leading-relaxed">
<span className="text-slate-900 font-bold block mb-1">خلاصه مدیریتی (Executive Summary):</span>
{item.executiveSummary}
</div>
</div>
{/* 2. WHY DOES IT MATTER & RELEVANCE */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="intel-card bg-white border border-slate-200 rounded-3xl p-6 shadow-sm flex flex-col justify-between gap-3">
<div>
<div className="flex items-center gap-2 text-teal-700 font-extrabold text-sm mb-2">
<Lightbulb size={17} className="text-teal-600" />
<span>چرا برای آموزش عالی اهمیت حیاتی دارد؟ (Why It Matters)</span>
</div>
<p className="text-slate-600 text-xs leading-relaxed">
{item.whyItMatters}
</p>
</div>
<div className="p-3 bg-teal-50 border border-teal-200 rounded-xl text-[11.5px] text-teal-800 font-medium">
نقطه عطف: این پدیده ساختارهای یادگیری و سنجش را دگرگون ساخته و انفعال در برابر آن موجب افت تقاضای داوطلبان خواهد شد.
</div>
</div>
<div className="intel-card bg-white border border-slate-200 rounded-3xl p-6 shadow-sm flex flex-col justify-between gap-3">
<div>
<div className="flex items-center gap-2 text-sky-700 font-extrabold text-sm mb-2">
<Building size={17} className="text-sky-600" />
<span>ارتباط مستقیم با وضعیت دانشگاه اصفهان</span>
</div>
<p className="text-slate-600 text-xs leading-relaxed">
{item.relevanceToUniversity}
</p>
</div>
<div className="p-3 bg-sky-50 border border-sky-200 rounded-xl text-[11.5px] text-sky-800 font-medium">
دانشکدهها و واحدهای درگیر: {item.implications.affectedDepartments.join('، ')}
</div>
</div>
</div>
{/* 3. MULTI-DIMENSIONAL IMPACT MODEL & TIME HORIZON */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
{/* Impact Model (7 Dimensions) */}
<div className="lg:col-span-7 intel-card bg-white border border-slate-200 rounded-3xl p-6 shadow-sm flex flex-col gap-4">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<h3 className="text-slate-900 font-bold text-sm flex items-center gap-2">
<Zap size={16} className="text-teal-600" />
<span>مدل تحلیلی اثرگذاری چندبُعدی بر ارکان دانشگاه اصفهان</span>
</h3>
<span className="badge badge-teal">مقیاس ۱ تا ۵</span>
</div>
<ImpactGauges impact={item.impact} />
</div>
{/* Time Horizon & Uncertainty Diagnosis */}
<div className="lg:col-span-5 intel-card bg-white border border-slate-200 rounded-3xl p-6 shadow-sm flex flex-col gap-4">
<div className="border-b border-slate-100 pb-3">
<h3 className="text-slate-900 font-bold text-sm flex items-center gap-2">
<Clock size={16} className="text-sky-600" />
<span>افق زمانی بلوغ و درجه عدم قطعیت</span>
</h3>
</div>
<div className="bg-slate-50 p-4 rounded-2xl border border-slate-200 text-xs flex flex-col gap-2.5">
<div className="flex items-center justify-between">
<span className="text-slate-500 font-medium">افق تحقق پیشبینیشده:</span>
<span className="font-bold text-teal-700">{getHorizonLabelFa(item.horizon)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-500 font-medium">شاخص عدم قطعیت:</span>
<span className="font-bold text-amber-700">{toPersianDigits(item.uncertaintyScore)} از ۵</span>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-500 font-medium">درجه اطمینان شواهد:</span>
<span className="font-bold text-sky-700">
{item.confidence === 'high' ? 'بالا (مبتنی بر اسناد موثق)' : 'متوسط'}
</span>
</div>
</div>
{/* Strategic Implications Grid (SWOT Bento Style) */}
<div className="flex flex-col gap-3">
<h4 className="text-slate-900 font-bold text-xs">تحلیل پیامدهای راهبردی (SWOT/Implications):</h4>
{/* Opportunities */}
<div className="bg-emerald-50 border border-emerald-200 p-3.5 rounded-2xl text-xs">
<span className="text-emerald-800 font-bold flex items-center gap-1.5 mb-1.5">
<CheckCircle2 size={15} />
فرصتهای پیشرو برای دانشگاه اصفهان:
</span>
<ul className="list-disc list-inside text-slate-700 space-y-1 text-[11.5px]">
{item.implications.opportunities.map((opp, idx) => (
<li key={idx}>{opp}</li>
))}
</ul>
</div>
{/* Risks & Threats */}
<div className="bg-rose-50 border border-rose-200 p-3.5 rounded-2xl text-xs">
<span className="text-rose-800 font-bold flex items-center gap-1.5 mb-1.5">
<AlertOctagon size={15} />
ریسکها و تهدیدهای پیشرو:
</span>
<ul className="list-disc list-inside text-slate-700 space-y-1 text-[11.5px]">
{item.implications.risks.map((risk, idx) => (
<li key={idx}>{risk}</li>
))}
{item.implications.threats.map((threat, idx) => (
<li key={`th-${idx}`}>{threat}</li>
))}
</ul>
</div>
{/* Required Capabilities */}
<div className="bg-slate-50 border border-slate-200 p-3.5 rounded-2xl text-xs">
<span className="text-slate-900 font-bold flex items-center gap-1.5 mb-1.5">
<ShieldCheck size={15} className="text-teal-600" />
قابلیتهای سازمانی و کالبدی موردنیاز:
</span>
<ul className="list-disc list-inside text-slate-700 space-y-1 text-[11.5px]">
{item.implications.requiredCapabilities.map((cap, idx) => (
<li key={idx}>{cap}</li>
))}
</ul>
</div>
</div>
</div>
</div>
{/* 4. EVIDENCE & SOURCES */}
<div className="intel-card bg-white border border-slate-200 rounded-3xl p-6 shadow-sm flex flex-col gap-4">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<h3 className="text-slate-900 font-bold text-sm flex items-center gap-2">
<FileText size={16} className="text-purple-600" />
<span>شواهد تجربی، مقالات و رخدادهای مؤید (Evidence & Sources)</span>
</h3>
<span className="text-slate-400 text-xs font-semibold">{toPersianDigits(item.evidence.length)} منبع مستند</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{item.evidence.map((ev, idx) => (
<div key={idx} className="bg-slate-50 border border-slate-200 p-4 rounded-2xl flex flex-col gap-2 text-xs">
<div className="flex items-center justify-between text-[11px] text-slate-500 font-bold">
<span className="text-teal-700">{ev.source}</span>
<span>{ev.date}</span>
</div>
<h5 className="text-slate-900 font-extrabold text-xs">{ev.title}</h5>
<p className="text-slate-600 leading-relaxed text-[11.5px] bg-white p-3 rounded-xl border border-slate-200/80">
«{ev.snippet}»
</p>
</div>
))}
</div>
</div>
{/* 5. STRATEGIC TRACEABILITY */}
<div className="intel-card bg-white border border-slate-200 rounded-3xl p-6 shadow-sm flex flex-col gap-5 border-t-4 border-t-teal-500">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<h3 className="text-slate-900 font-black text-sm flex items-center gap-2">
<Layers size={17} className="text-teal-600" />
<span>زنجیره ارتباطات راهبردی و ردپای اثر (Strategic Traceability)</span>
</h3>
<span className="text-slate-400 text-xs">اتصالات پایگاه داده هوشمندی</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 text-xs">
{/* Linked 1415 Dimensions */}
<div className="flex flex-col gap-2.5">
<div className="text-teal-700 font-bold flex items-center gap-1.5">
<Target size={14} />
ابعاد مرتبط در دانشگاه اصفهان ۱۴۱۵:
</div>
<div className="flex flex-col gap-2">
{relatedDimensions.map((dim) => (
<div
key={dim.id}
onClick={() => onSelectDimension(dim)}
className="bg-slate-50 hover:bg-slate-100 border border-slate-200 p-3 rounded-2xl cursor-pointer transition-all flex items-center justify-between shadow-sm"
>
<span className="text-slate-900 font-bold">{dim.title}</span>
<span className="text-rose-600 font-bold text-[11px]">شکاف: {toPersianDigits(dim.gap)}</span>
</div>
))}
</div>
</div>
{/* Linked Jobs */}
<div className="flex flex-col gap-2.5">
<div className="text-amber-700 font-bold flex items-center gap-1.5">
<Briefcase size={14} />
مشاغل تحت تأثیر:
</div>
<div className="flex flex-col gap-2">
{relatedJobs.map((job) => (
<div
key={job.id}
className="bg-slate-50 border border-slate-200 p-3 rounded-2xl flex items-center justify-between shadow-sm"
>
<span className="text-slate-800 font-bold">{job.title}</span>
<span className={`badge ${job.type === 'growing' ? 'badge-teal' : 'badge-rose'}`}>
{job.type === 'growing' ? 'صعودی' : 'در تحول'}
</span>
</div>
))}
{relatedJobs.length === 0 && (
<span className="text-slate-400 text-[11px] italic">مشاغل عمومی آموزش عالی</span>
)}
</div>
</div>
{/* Linked Skills */}
<div className="flex flex-col gap-2.5">
<div className="text-purple-700 font-bold flex items-center gap-1.5">
<Brain size={14} />
شایستگیهای موردنیاز:
</div>
<div className="flex flex-col gap-2">
{relatedSkills.map((sk) => (
<div
key={sk.id}
className="bg-slate-50 border border-slate-200 p-3 rounded-2xl flex items-center justify-between shadow-sm"
>
<span className="text-slate-800 font-bold">{sk.title}</span>
<span className="text-slate-500 text-[11px]">اهمیت: {toPersianDigits(sk.importanceScore)}/۵</span>
</div>
))}
{relatedSkills.length === 0 && (
<span className="text-slate-400 text-[11px] italic">مهارتهای تفکر سیستمی و دیجیتال</span>
)}
</div>
</div>
</div>
{/* Other Related Trends / Tech */}
{relatedItems.length > 0 && (
<div className="pt-4 border-t border-slate-100 flex flex-col gap-2">
<span className="text-slate-500 text-xs font-bold">سایر موارد همبسته در رادار:</span>
<div className="flex flex-wrap gap-2">
{relatedItems.map((rItem) => (
<button
key={rItem.id}
onClick={() => onSelectItem(rItem)}
className="bg-slate-100 hover:bg-slate-200/80 border border-slate-200 text-slate-800 px-3.5 py-1.5 rounded-full text-xs transition-colors flex items-center gap-1.5 shadow-sm font-semibold"
>
<span>{rItem.title}</span>
<ArrowRight size={12} className="text-teal-600" />
</button>
))}
</div>
</div>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,295 @@
import React, { useState, useMemo } from 'react';
import { JobEntity, SkillEntity, Horizon } from '../types/futures';
import { JobCard, SkillCard } from '../components/cards/JobSkillCard';
import { LaborCharts } from '../components/visual/LaborCharts';
import { toPersianDigits } from '../utils/persianNumbers';
import {
Briefcase,
TrendingUp,
AlertTriangle,
Brain,
Filter,
Search,
School,
Sparkles
} from 'lucide-react';
interface LaborMarketPageProps {
jobs: JobEntity[];
skills: SkillEntity[];
}
export const LaborMarketPage: React.FC<LaborMarketPageProps> = ({ jobs, skills }) => {
const [activeQuadrant, setActiveQuadrant] = useState<
'all' | 'growing_jobs' | 'transforming_jobs' | 'growing_skills' | 'declining_skills'
>('all');
const [selectedDiscipline, setSelectedDiscipline] = useState<string>('all');
const [selectedHorizon, setSelectedHorizon] = useState<Horizon | 'all'>('all');
const [searchQuery, setSearchQuery] = useState('');
// Extract unique disciplines
const disciplines = useMemo(() => {
const set = new Set<string>();
jobs.forEach((j) => {
if (j.discipline) set.add(j.discipline);
});
return Array.from(set);
}, [jobs]);
const filteredJobs = useMemo(() => {
return jobs.filter((j) => {
if (selectedHorizon !== 'all' && j.horizon !== selectedHorizon) return false;
if (selectedDiscipline !== 'all' && j.discipline !== selectedDiscipline) return false;
if (searchQuery.trim() !== '') {
const q = searchQuery.toLowerCase().trim();
const inTitle = j.title.toLowerCase().includes(q);
const inDesc = j.description.toLowerCase().includes(q);
const inDisc = j.discipline.toLowerCase().includes(q);
if (!inTitle && !inDesc && !inDisc) return false;
}
return true;
});
}, [jobs, selectedHorizon, selectedDiscipline, searchQuery]);
const filteredSkills = useMemo(() => {
return skills.filter((s) => {
if (searchQuery.trim() !== '') {
const q = searchQuery.toLowerCase().trim();
const inTitle = s.title.toLowerCase().includes(q);
const inDesc = s.description.toLowerCase().includes(q);
if (!inTitle && !inDesc) return false;
}
return true;
});
}, [skills, searchQuery]);
const growingJobs = filteredJobs.filter((j) => j.type === 'growing');
const transformingJobs = filteredJobs.filter((j) => j.type === 'transforming');
const growingSkills = filteredSkills.filter((s) => s.type === 'growing');
const decliningSkills = filteredSkills.filter((s) => s.type === 'declining');
return (
<div className="flex flex-col gap-6">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-2 border-b border-slate-800">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="badge badge-amber">تحلیل اشتغالپذیری و آمایش رشتهها</span>
<span className="text-slate-400 text-xs">دیدهبانی مهارتهای نوظهور</span>
</div>
<h1 className="page-title text-2xl text-white">
بازار کار آینده و تحول مشاغل (Future Labor Market)
</h1>
<p className="page-subtitle text-slate-300 text-xs mt-1">
رصد ۴ ربع کلیدی تحول تقاضای شغلی: مشاغل رو به رشد، مشاغل در معرض دگرگونی، مهارتهای صعودی و مهارتهای منسوخ شونده
</p>
</div>
<div className="flex items-center gap-2">
<span className="text-xs bg-[#0c1935] border border-teal-500/30 text-teal-300 px-3 py-1.5 rounded-xl font-bold">
{toPersianDigits(jobs.length)} عنوان شغل رصد شده
</span>
<span className="text-xs bg-[#0c1935] border border-purple-500/30 text-purple-300 px-3 py-1.5 rounded-xl font-bold">
{toPersianDigits(skills.length)} مهارت کلیدی
</span>
</div>
</div>
{/* Analytical Charts Section */}
<LaborCharts jobs={jobs} skills={skills} />
{/* Filters Bar */}
<div className="intel-card p-4 flex flex-col gap-3">
<div className="flex flex-wrap items-center justify-between gap-3">
{/* Quadrant Pills */}
<div className="flex flex-wrap items-center gap-2 text-xs">
<button
onClick={() => setActiveQuadrant('all')}
className={`px-3 py-1.5 rounded-xl font-semibold transition-all ${
activeQuadrant === 'all'
? 'bg-teal-500 text-slate-950 font-bold'
: 'bg-[#091122] text-slate-300 border border-slate-800 hover:bg-[#111f3d]'
}`}
>
همه ربعها
</button>
<button
onClick={() => setActiveQuadrant('growing_jobs')}
className={`px-3 py-1.5 rounded-xl font-semibold transition-all flex items-center gap-1.5 ${
activeQuadrant === 'growing_jobs'
? 'bg-teal-400 text-slate-950 font-bold'
: 'bg-[#091122] text-teal-400 border border-teal-950 hover:bg-[#111f3d]'
}`}
>
<TrendingUp size={13} />
مشاغل رو به رشد ({toPersianDigits(growingJobs.length)})
</button>
<button
onClick={() => setActiveQuadrant('transforming_jobs')}
className={`px-3 py-1.5 rounded-xl font-semibold transition-all flex items-center gap-1.5 ${
activeQuadrant === 'transforming_jobs'
? 'bg-rose-500 text-slate-950 font-bold'
: 'bg-[#091122] text-rose-400 border border-rose-950 hover:bg-[#111f3d]'
}`}
>
<AlertTriangle size={13} />
مشاغل در معرض تغییر ({toPersianDigits(transformingJobs.length)})
</button>
<button
onClick={() => setActiveQuadrant('growing_skills')}
className={`px-3 py-1.5 rounded-xl font-semibold transition-all flex items-center gap-1.5 ${
activeQuadrant === 'growing_skills'
? 'bg-purple-500 text-slate-950 font-bold'
: 'bg-[#091122] text-purple-400 border border-purple-950 hover:bg-[#111f3d]'
}`}
>
<Brain size={13} />
مهارتهای رو به رشد ({toPersianDigits(growingSkills.length)})
</button>
<button
onClick={() => setActiveQuadrant('declining_skills')}
className={`px-3 py-1.5 rounded-xl font-semibold transition-all flex items-center gap-1.5 ${
activeQuadrant === 'declining_skills'
? 'bg-amber-500 text-slate-950 font-bold'
: 'bg-[#091122] text-amber-400 border border-amber-950 hover:bg-[#111f3d]'
}`}
>
<Sparkles size={13} />
مهارتهای در حال کاهش ({toPersianDigits(decliningSkills.length)})
</button>
</div>
{/* Search */}
<div className="relative w-full sm:w-64">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="جستجو در مشاغل و مهارت‌ها..."
className="w-full bg-[#081020] border border-slate-800 rounded-xl py-1.5 pr-8 pl-3 text-xs text-white outline-none focus:border-teal-500 placeholder:text-slate-500"
/>
<Search size={14} className="absolute right-2.5 top-2.5 text-slate-400" />
</div>
</div>
{/* Discipline / Faculty Filter */}
<div className="flex flex-wrap items-center gap-2 pt-2 border-t border-slate-800/80 text-xs">
<span className="text-slate-400 font-semibold flex items-center gap-1">
<School size={13} />
فیلتر بر اساس دانشکده / رشته دانشگاه اصفهان:
</span>
<select
value={selectedDiscipline}
onChange={(e) => setSelectedDiscipline(e.target.value)}
className="bg-[#091122] border border-slate-800 text-slate-200 text-xs rounded-lg p-1.5 outline-none focus:border-teal-500"
>
<option value="all">همه دانشکدهها و گروههای آموزشی</option>
{disciplines.map((d, idx) => (
<option key={idx} value={d}>
{d}
</option>
))}
</select>
</div>
</div>
{/* 4 Quadrants Display */}
{/* Quadrant 1: Growing Jobs */}
{(activeQuadrant === 'all' || activeQuadrant === 'growing_jobs') && (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between border-r-4 border-r-teal-400 pr-3">
<div>
<h3 className="text-white font-extrabold text-base flex items-center gap-2">
<TrendingUp size={18} className="text-teal-400" />
<span>مشاغل رو به رشد (Growing Occupations)</span>
</h3>
<p className="text-slate-400 text-xs mt-0.5">
مشاغلی که تقاضای بازار کار محلی، ملی و بینالمللی برای آنها با نرخ شتابان در حال افزایش است
</p>
</div>
<span className="badge badge-teal">{toPersianDigits(growingJobs.length)} شغل</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{growingJobs.map((job) => (
<JobCard key={job.id} job={job} />
))}
</div>
</div>
)}
{/* Quadrant 2: Transforming Jobs */}
{(activeQuadrant === 'all' || activeQuadrant === 'transforming_jobs') && (
<div className="flex flex-col gap-3 mt-4">
<div className="flex items-center justify-between border-r-4 border-r-rose-400 pr-3">
<div>
<h3 className="text-white font-extrabold text-base flex items-center gap-2">
<AlertTriangle size={18} className="text-rose-400" />
<span>مشاغل در معرض تغییر و بازآفرینی (Transforming Occupations)</span>
</h3>
<p className="text-slate-400 text-xs mt-0.5">
مشاغل سنتی با ریسک اتوماسیون بالا که برای بقا نیازمند بازطراحی مهارتی و همآفرینی با فناوری هستند
</p>
</div>
<span className="badge badge-rose">{toPersianDigits(transformingJobs.length)} شغل</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{transformingJobs.map((job) => (
<JobCard key={job.id} job={job} />
))}
</div>
</div>
)}
{/* Quadrant 3: Growing Skills */}
{(activeQuadrant === 'all' || activeQuadrant === 'growing_skills') && (
<div className="flex flex-col gap-3 mt-4">
<div className="flex items-center justify-between border-r-4 border-r-purple-400 pr-3">
<div>
<h3 className="text-white font-extrabold text-base flex items-center gap-2">
<Brain size={18} className="text-purple-400" />
<span>مهارتهای رو به رشد (Rising Skills)</span>
</h3>
<p className="text-slate-400 text-xs mt-0.5">
شایستگیهای کلیدی شناختی، دیجیتال، بینرشتهای و انسانی که ضامن اشتغالپذیری فارغالتحصیلان است
</p>
</div>
<span className="badge badge-purple">{toPersianDigits(growingSkills.length)} مهارت</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{growingSkills.map((skill) => (
<SkillCard key={skill.id} skill={skill} />
))}
</div>
</div>
)}
{/* Quadrant 4: Declining Skills */}
{(activeQuadrant === 'all' || activeQuadrant === 'declining_skills') && (
<div className="flex flex-col gap-3 mt-4">
<div className="flex items-center justify-between border-r-4 border-r-amber-400 pr-3">
<div>
<h3 className="text-white font-extrabold text-base flex items-center gap-2">
<Sparkles size={18} className="text-amber-400" />
<span>مهارتهای در حال کاهش و منسوخ شونده (Declining Skills)</span>
</h3>
<p className="text-slate-400 text-xs mt-0.5">
مهارتهای مبتنی بر محفوظات، ورود دستی دادهها و روتینهای محاسباتی که ارزش بازار کار را از دست میدهند
</p>
</div>
<span className="badge badge-amber">{toPersianDigits(decliningSkills.length)} مهارت</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{decliningSkills.map((skill) => (
<SkillCard key={skill.id} skill={skill} />
))}
</div>
</div>
)}
</div>
);
};

372
src/pages/Login.tsx Normal file
View File

@ -0,0 +1,372 @@
import React, { useState } from 'react';
import { authService, PRESET_USERS } from '../services/authService';
import { PersonaType } from '../types/futures';
import { ForgotPasswordModal } from '../components/modals/ForgotPasswordModal';
import { Compass, Shield, KeyRound, Smartphone, CheckCircle, AlertCircle, ArrowLeft } from 'lucide-react';
interface LoginProps {
onLoginSuccess: () => void;
}
export const Login: React.FC<LoginProps> = ({ onLoginSuccess }) => {
const [activeTab, setActiveTab] = useState<'password' | 'otp' | 'persona'>('password');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [mobileNumber, setMobileNumber] = useState('');
const [otpCode, setOtpCode] = useState('');
const [otpSent, setOtpSent] = useState(false);
const [simulatedCode, setSimulatedCode] = useState('');
const [loading, setLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState('');
const [showForgotModal, setShowForgotModal] = useState(false);
const handlePasswordLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (!username.trim()) return;
setLoading(true);
setErrorMsg('');
try {
await authService.loginWithCredentials(username, password);
setLoading(false);
onLoginSuccess();
} catch (err: any) {
setLoading(false);
setErrorMsg(err.message || 'خطا در ورود');
}
};
const handleSendOtp = async (e: React.FormEvent) => {
e.preventDefault();
if (!mobileNumber.trim()) return;
setLoading(true);
setErrorMsg('');
try {
const res = await authService.sendOtp(mobileNumber);
setOtpSent(true);
setSimulatedCode(res.simulatedOtp);
setLoading(false);
} catch (err: any) {
setLoading(false);
setErrorMsg('خطا در ارسال پیامک کد یکبار مصرف');
}
};
const handleVerifyOtp = async (e: React.FormEvent) => {
e.preventDefault();
if (!otpCode.trim()) return;
setLoading(true);
setErrorMsg('');
try {
await authService.verifyOtp(otpCode);
setLoading(false);
onLoginSuccess();
} catch (err: any) {
setLoading(false);
setErrorMsg(err.message || 'کد وارد شده نامعتبر است');
}
};
const handleQuickPersonaLogin = (role: PersonaType) => {
authService.switchPersona(role);
onLoginSuccess();
};
return (
<div className="min-h-screen w-full flex items-center justify-center p-4 bg-radial-gradient">
<div className="w-full max-w-md intel-card border-[#1f3258] bg-[#0c162b]/95 p-6 lg:p-8 shadow-2xl backdrop-blur-xl animate-fadeIn">
{/* Institutional Emblem & Title */}
<div className="flex flex-col items-center text-center mb-6">
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#0c2452] to-[#12397c] border border-teal-400/50 flex items-center justify-center mb-3 shadow-xl shadow-teal-500/20">
<Compass size={32} className="text-teal-400" />
</div>
<h2 className="text-white font-black text-xl lg:text-2xl tracking-tight">
رادار آینده دانشگاه اصفهان
</h2>
<p className="text-slate-400 text-xs mt-1">
سامانه پایش هوشمندی راهبردی و آیندهپژوهی آموزش عالی
</p>
<div className="mt-2 inline-flex items-center gap-1 bg-teal-500/10 text-teal-300 border border-teal-500/30 px-2.5 py-0.5 rounded-full text-[11px] font-semibold">
<Shield size={12} />
درگاه احراز هویت یکپارچه دانشگاهی
</div>
</div>
{/* Tab Switcher */}
<div className="grid grid-cols-3 gap-1 bg-[#081020] p-1 rounded-xl mb-5 text-xs font-semibold">
<button
onClick={() => {
setActiveTab('password');
setErrorMsg('');
}}
className={`py-1.5 rounded-lg transition-all ${
activeTab === 'password'
? 'bg-teal-500 text-slate-950 font-bold shadow-md'
: 'text-slate-400 hover:text-white'
}`}
>
کلمه عبور
</button>
<button
onClick={() => {
setActiveTab('otp');
setErrorMsg('');
}}
className={`py-1.5 rounded-lg transition-all ${
activeTab === 'otp'
? 'bg-teal-500 text-slate-950 font-bold shadow-md'
: 'text-slate-400 hover:text-white'
}`}
>
کد یکبار مصرف
</button>
<button
onClick={() => {
setActiveTab('persona');
setErrorMsg('');
}}
className={`py-1.5 rounded-lg transition-all ${
activeTab === 'persona'
? 'bg-teal-500 text-slate-950 font-bold shadow-md'
: 'text-slate-400 hover:text-white'
}`}
>
ورود سریع نقشی
</button>
</div>
{errorMsg && (
<div className="mb-4 p-3 bg-rose-950/40 border border-rose-500/50 rounded-xl text-rose-300 text-xs flex items-center gap-2">
<AlertCircle size={15} className="shrink-0" />
<span>{errorMsg}</span>
</div>
)}
{/* Tab 1: Username / Password Form */}
{activeTab === 'password' && (
<form onSubmit={handlePasswordLogin} className="flex flex-col gap-4 text-xs">
<div className="flex flex-col gap-1.5 text-right">
<label className="text-slate-300 font-semibold">شناسه کاربری دانشگاهی:</label>
<input
type="text"
required
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="president یا researcher یا نام کاربری..."
className="w-full bg-[#081020] border border-slate-800 rounded-xl p-3 text-white outline-none focus:border-teal-500 text-xs text-right"
/>
<span className="text-[10px] text-slate-500">
(راهنما: برای ورود با هر نام کاربری میتوانید تست نمایید)
</span>
</div>
<div className="flex flex-col gap-1.5 text-right">
<div className="flex items-center justify-between">
<label className="text-slate-300 font-semibold">رمز عبور:</label>
<button
type="button"
onClick={() => setShowForgotModal(true)}
className="text-[11px] text-teal-400 hover:underline"
>
فراموشی رمز عبور؟
</button>
</div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="w-full bg-[#081020] border border-slate-800 rounded-xl p-3 text-white outline-none focus:border-teal-500 text-xs"
/>
</div>
<button
type="submit"
disabled={loading}
className="btn btn-primary w-full py-3 mt-2 font-bold text-xs flex items-center justify-center gap-2"
>
{loading ? (
<span>در حال اعتبارسنجی...</span>
) : (
<>
<KeyRound size={15} />
<span>ورود امن به سامانه آیندهپژوهی</span>
</>
)}
</button>
</form>
)}
{/* Tab 2: OTP Login Flow */}
{activeTab === 'otp' && (
<div className="flex flex-col gap-4 text-xs text-right">
{!otpSent ? (
<form onSubmit={handleSendOtp} className="flex flex-col gap-4">
<p className="text-slate-300 leading-relaxed text-[11.5px]">
کد ۵ رقمی یکبار مصرف به شماره تلفن همراه ثبتشده شما در اتوماسیون دانشگاه ارسال خواهد شد.
</p>
<div className="flex flex-col gap-1.5">
<label className="text-slate-300 font-semibold">شماره تلفن همراه:</label>
<div className="relative">
<input
type="tel"
required
value={mobileNumber}
onChange={(e) => setMobileNumber(e.target.value)}
placeholder="0913xxxxxxx"
className="w-full bg-[#081020] border border-slate-800 rounded-xl p-3 text-white outline-none focus:border-teal-500 text-xs text-left font-mono"
/>
<Smartphone size={16} className="absolute left-3 top-3 text-slate-500" />
</div>
</div>
<button
type="submit"
disabled={loading}
className="btn btn-primary w-full py-3 mt-2 font-bold text-xs"
>
{loading ? 'در حال ارسال...' : 'ارسال کد تأیید پیامکی'}
</button>
</form>
) : (
<form onSubmit={handleVerifyOtp} className="flex flex-col gap-4 animate-fadeIn">
<div className="p-3 bg-teal-950/30 border border-teal-500/40 rounded-xl text-teal-300 text-[11.5px] flex items-center gap-2">
<CheckCircle size={16} className="shrink-0 text-teal-400" />
<div>
کد یکبار مصرف شبیهسازی شده آزمایشی:{' '}
<strong className="font-mono text-white text-sm bg-teal-900/60 px-2 py-0.5 rounded">
{simulatedCode}
</strong>
</div>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-slate-300 font-semibold">کد تأیید پیامکشده:</label>
<input
type="text"
required
maxLength={5}
autoFocus
value={otpCode}
onChange={(e) => setOtpCode(e.target.value)}
placeholder="کد ۵ رقمی را وارد کنید"
className="w-full bg-[#081020] border border-slate-800 rounded-xl p-3 text-white outline-none focus:border-teal-500 text-sm text-center font-mono tracking-widest"
/>
</div>
<button
type="submit"
disabled={loading}
className="btn btn-primary w-full py-3 font-bold text-xs"
>
{loading ? 'در حال بررسی...' : 'تأیید کد و ورود به سامانه'}
</button>
<button
type="button"
onClick={() => setOtpSent(false)}
className="text-slate-400 hover:text-white text-center text-[11px] mt-1"
>
ویرایش شماره تلفن همراه
</button>
</form>
)}
</div>
)}
{/* Tab 3: Quick Persona Selection (For University Decision-Makers Testing) */}
{activeTab === 'persona' && (
<div className="flex flex-col gap-2.5 animate-fadeIn">
<p className="text-slate-400 text-[11px] mb-1 text-right">
جهت ارزیابی فوری پلتفرم، یکی از نقشهای سازمانی زیر را انتخاب فرمایید:
</p>
{/* Persona 1: President */}
<button
onClick={() => handleQuickPersonaLogin('president')}
className="bg-[#0b1428] hover:bg-[#122242] border border-teal-500/40 p-3 rounded-xl text-right flex items-center justify-between transition-all group"
>
<div>
<div className="font-bold text-white text-xs group-hover:text-teal-300">
{PRESET_USERS.president.fullName}
</div>
<div className="text-[11px] text-teal-400 mt-0.5">
{PRESET_USERS.president.roleTitleFa}
</div>
<div className="text-[10px] text-slate-400 mt-0.5">
دیدگاه: پیامدهای کلان و راهبردی برای دانشگاه
</div>
</div>
<ArrowLeft size={16} className="text-teal-400 group-hover:-translate-x-1 transition-transform" />
</button>
{/* Persona 2: Researcher */}
<button
onClick={() => handleQuickPersonaLogin('researcher')}
className="bg-[#0b1428] hover:bg-[#122242] border border-sky-500/40 p-3 rounded-xl text-right flex items-center justify-between transition-all group"
>
<div>
<div className="font-bold text-white text-xs group-hover:text-sky-300">
{PRESET_USERS.researcher.fullName}
</div>
<div className="text-[11px] text-sky-400 mt-0.5">
{PRESET_USERS.researcher.roleTitleFa}
</div>
<div className="text-[10px] text-slate-400 mt-0.5">
دیدگاه: رصد سیگنالهای ضعیف، شواهد و پیشرانها
</div>
</div>
<ArrowLeft size={16} className="text-sky-400 group-hover:-translate-x-1 transition-transform" />
</button>
{/* Persona 3: Strategic Planner */}
<button
onClick={() => handleQuickPersonaLogin('planner')}
className="bg-[#0b1428] hover:bg-[#122242] border border-amber-500/40 p-3 rounded-xl text-right flex items-center justify-between transition-all group"
>
<div>
<div className="font-bold text-white text-xs group-hover:text-amber-300">
{PRESET_USERS.planner.fullName}
</div>
<div className="text-[11px] text-amber-400 mt-0.5">
{PRESET_USERS.planner.roleTitleFa}
</div>
<div className="text-[10px] text-slate-400 mt-0.5">
دیدگاه: تحلیل شکاف وضع موجود و چشمانداز ۱۴۱۵
</div>
</div>
<ArrowLeft size={16} className="text-amber-400 group-hover:-translate-x-1 transition-transform" />
</button>
{/* Persona 4: System Admin */}
<button
onClick={() => handleQuickPersonaLogin('admin')}
className="bg-[#0b1428] hover:bg-[#122242] border border-purple-500/40 p-3 rounded-xl text-right flex items-center justify-between transition-all group"
>
<div>
<div className="font-bold text-white text-xs group-hover:text-purple-300">
{PRESET_USERS.admin.fullName}
</div>
<div className="text-[11px] text-purple-400 mt-0.5">
{PRESET_USERS.admin.roleTitleFa}
</div>
<div className="text-[10px] text-slate-400 mt-0.5">
مدیریت دادهها، ویرایش سیگنالها و کالیبراسیون
</div>
</div>
<ArrowLeft size={16} className="text-purple-400 group-hover:-translate-x-1 transition-transform" />
</button>
</div>
)}
<div className="mt-6 pt-4 border-t border-slate-800/80 text-center text-[10.5px] text-slate-500">
مرکز رصد فناوری و آیندهپژوهی دانشگاه اصفهان دسترسی ویژه مدیران و پژوهشگران
</div>
</div>
<ForgotPasswordModal
isOpen={showForgotModal}
onClose={() => setShowForgotModal(false)}
/>
</div>
);
};

337
src/pages/RadarPage.tsx Normal file
View File

@ -0,0 +1,337 @@
import React, { useState, useMemo } from 'react';
import { IntelligenceItem, Category, Horizon } from '../types/futures';
import { FuturesRadar } from '../components/visual/FuturesRadar';
import { IntelligenceCard } from '../components/cards/IntelligenceCard';
import {
toPersianDigits,
getCategoryLabelFa,
getHorizonLabelFa,
getCategoryColor
} from '../utils/persianNumbers';
import {
Radar,
Grid,
Table,
Search,
Clock,
Zap,
Filter,
Plus
} from 'lucide-react';
interface RadarPageProps {
items: IntelligenceItem[];
onSelectItem: (item: IntelligenceItem) => void;
onOpenCreateModal: () => void;
initialCategory?: Category | 'all';
}
export const RadarPage: React.FC<RadarPageProps> = ({
items,
onSelectItem,
onOpenCreateModal,
initialCategory = 'all'
}) => {
const [selectedCategory, setSelectedCategory] = useState<Category | 'all'>(initialCategory);
const [selectedHorizon, setSelectedHorizon] = useState<Horizon | 'all'>('all');
const [minImpact, setMinImpact] = useState<number>(1);
const [searchQuery, setSearchQuery] = useState<string>('');
const [viewMode, setViewMode] = useState<'radar' | 'grid' | 'table'>('radar');
const filteredItems = useMemo(() => {
return items.filter((item) => {
if (selectedCategory !== 'all' && item.category !== selectedCategory) return false;
if (selectedHorizon !== 'all' && item.horizon !== selectedHorizon) return false;
if (item.impactScore < minImpact) return false;
if (searchQuery.trim() !== '') {
const q = searchQuery.toLowerCase().trim();
const inTitle = item.title.toLowerCase().includes(q);
const inSummary = item.executiveSummary.toLowerCase().includes(q);
const inWhy = item.whyItMatters.toLowerCase().includes(q);
if (!inTitle && !inSummary && !inWhy) return false;
}
return true;
});
}, [items, selectedCategory, selectedHorizon, minImpact, searchQuery]);
return (
<div className="flex flex-col gap-6 animate-fadeIn">
{/* Page Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-2 border-b border-slate-200">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="badge badge-teal">سامانه رصد مستمر</span>
<span className="text-slate-400 text-xs font-medium">دیدهبانی آینده آموزش عالی</span>
</div>
<h1 className="page-title text-2xl lg:text-3xl text-slate-900">
رادار آینده دانشگاه (Futures Radar)
</h1>
<p className="page-subtitle text-slate-500 text-xs mt-1">
دیدهبانی جامع و پایش چندبُعدی روندها، فناوریها، سیاستها و سیگنالهای ضعیف تغییر
</p>
</div>
<div className="flex items-center gap-3">
{/* View Mode Toggle (Pill Switcher) */}
<div className="flex items-center bg-slate-100 border border-slate-200 p-1 rounded-full text-xs shadow-sm">
<button
onClick={() => setViewMode('radar')}
className={`py-1.5 px-3 rounded-full flex items-center gap-1.5 transition-all ${
viewMode === 'radar'
? 'bg-slate-900 text-white font-bold shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
title="نمای رادار دایره‌ای"
>
<Radar size={15} />
<span className="hidden sm:inline">رادار دایرهای</span>
</button>
<button
onClick={() => setViewMode('grid')}
className={`py-1.5 px-3 rounded-full flex items-center gap-1.5 transition-all ${
viewMode === 'grid'
? 'bg-slate-900 text-white font-bold shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
title="نمای شبکه کارت‌ها"
>
<Grid size={15} />
<span className="hidden sm:inline">شبکه کارتها</span>
</button>
<button
onClick={() => setViewMode('table')}
className={`py-1.5 px-3 rounded-full flex items-center gap-1.5 transition-all ${
viewMode === 'table'
? 'bg-slate-900 text-white font-bold shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
title="نمای جدول تحلیلی"
>
<Table size={15} />
<span className="hidden sm:inline">جدول تحلیلی</span>
</button>
</div>
<button
onClick={onOpenCreateModal}
className="btn btn-primary text-xs py-2 px-4 flex items-center gap-1.5 shadow-sm"
>
<Plus size={15} />
<span>ثبت سیگنال جدید</span>
</button>
</div>
</div>
{/* Analytical Filter Controls Bar (Bento White Card) */}
<div className="intel-card p-5 bg-white border border-slate-200 rounded-3xl shadow-sm flex flex-col gap-4">
{/* Category Pills */}
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap items-center gap-2 text-xs">
<span className="text-slate-500 font-bold flex items-center gap-1 ml-1">
<Filter size={13} />
دستهبندی موضوعی:
</span>
<button
onClick={() => setSelectedCategory('all')}
className={`px-3 py-1.5 rounded-full font-bold transition-all ${
selectedCategory === 'all'
? 'bg-slate-900 text-white shadow-sm'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200/80'
}`}
>
همه ({toPersianDigits(items.length)})
</button>
<button
onClick={() => setSelectedCategory('trend')}
className={`px-3 py-1.5 rounded-full font-bold transition-all ${
selectedCategory === 'trend'
? 'bg-sky-600 text-white shadow-sm'
: 'bg-sky-50 text-sky-700 border border-sky-200 hover:bg-sky-100'
}`}
>
روندها (Trends)
</button>
<button
onClick={() => setSelectedCategory('technology')}
className={`px-3 py-1.5 rounded-full font-bold transition-all ${
selectedCategory === 'technology'
? 'bg-teal-600 text-white shadow-sm'
: 'bg-teal-50 text-teal-700 border border-teal-200 hover:bg-teal-100'
}`}
>
فناوریها (Tech)
</button>
<button
onClick={() => setSelectedCategory('policy')}
className={`px-3 py-1.5 rounded-full font-bold transition-all ${
selectedCategory === 'policy'
? 'bg-amber-600 text-white shadow-sm'
: 'bg-amber-50 text-amber-800 border border-amber-200 hover:bg-amber-100'
}`}
>
سیاستها (Policies)
</button>
<button
onClick={() => setSelectedCategory('weak_signal')}
className={`px-3 py-1.5 rounded-full font-bold transition-all ${
selectedCategory === 'weak_signal'
? 'bg-rose-600 text-white shadow-sm'
: 'bg-rose-50 text-rose-700 border border-rose-200 hover:bg-rose-100'
}`}
>
سیگنالهای آینده (Signals)
</button>
</div>
{/* Quick Search Pill Input */}
<div className="relative w-full sm:w-64">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="فیلتر با واژگان کلیدی..."
className="w-full bg-slate-100 border border-slate-200 rounded-full py-1.5 pr-8 pl-3 text-xs text-slate-900 outline-none focus:border-teal-500 placeholder:text-slate-400 shadow-sm"
/>
<Search size={14} className="absolute right-3 top-2.5 text-slate-400" />
</div>
</div>
{/* Horizon & Impact Sliders */}
<div className="flex flex-wrap items-center justify-between gap-4 pt-3 border-t border-slate-100 text-xs">
{/* Horizon Selector */}
<div className="flex flex-wrap items-center gap-2">
<span className="text-slate-500 font-bold flex items-center gap-1">
<Clock size={13} />
افق زمانی تحقق:
</span>
{(['all', 'now', 'near', 'mid', 'long'] as const).map((h) => (
<button
key={h}
onClick={() => setSelectedHorizon(h)}
className={`px-3 py-1 rounded-full font-semibold transition-all ${
selectedHorizon === h
? 'bg-slate-800 text-white font-bold'
: 'bg-slate-100 text-slate-600 hover:text-slate-900 hover:bg-slate-200'
}`}
>
{h === 'all' ? 'همه افق‌ها' : getHorizonLabelFa(h)}
</button>
))}
</div>
{/* Impact Score Threshold */}
<div className="flex items-center gap-2">
<span className="text-slate-500 font-bold flex items-center gap-1">
<Zap size={13} className="text-teal-600" />
حداقل شدت اثر:
</span>
<input
type="range"
min={1}
max={5}
value={minImpact}
onChange={(e) => setMinImpact(Number(e.target.value))}
className="w-24 accent-teal-600 cursor-pointer"
/>
<span className="font-bold text-slate-800 font-mono bg-slate-100 px-2.5 py-0.5 rounded-full border border-slate-200">
{toPersianDigits(minImpact)} از ۵
</span>
</div>
</div>
</div>
{/* Active Results Counter */}
<div className="flex items-center justify-between text-xs text-slate-500 px-1 font-medium">
<span>
نمایش <strong className="text-slate-900 font-bold">{toPersianDigits(filteredItems.length)}</strong> مورد از مجموع{' '}
<strong className="text-slate-900 font-bold">{toPersianDigits(items.length)}</strong> موجودیت پایگاه رصد
</span>
</div>
{/* View Mode 1: Interactive Circular Radar */}
{viewMode === 'radar' && (
<div className="intel-card p-8 bg-white border border-slate-200 rounded-3xl shadow-sm flex flex-col items-center justify-center animate-fadeIn">
<FuturesRadar
items={filteredItems}
selectedCategory={selectedCategory}
selectedHorizon={selectedHorizon}
onSelectItem={onSelectItem}
height={680}
/>
</div>
)}
{/* View Mode 2: Card Grid */}
{viewMode === 'grid' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 animate-fadeIn">
{filteredItems.map((item) => (
<IntelligenceCard
key={item.id}
item={item}
onClick={() => onSelectItem(item)}
/>
))}
{filteredItems.length === 0 && (
<div className="col-span-full py-16 text-center text-slate-400 text-sm">
هیچ موردی منطبق با فیلترهای انتخابی یافت نشد.
</div>
)}
</div>
)}
{/* View Mode 3: Matrix Table */}
{viewMode === 'table' && (
<div className="intel-card overflow-x-auto p-0 bg-white border border-slate-200 rounded-3xl shadow-sm animate-fadeIn">
<table className="w-full text-right text-xs border-collapse">
<thead>
<tr className="bg-slate-50 text-slate-500 border-b border-slate-200">
<th className="p-4 font-bold">عنوان پدیده / سیگنال</th>
<th className="p-4 font-bold">دستهبندی</th>
<th className="p-4 font-bold">افق زمانی</th>
<th className="p-4 font-bold">شدت اثر</th>
<th className="p-4 font-bold">عدم قطعیت</th>
<th className="p-4 font-bold">دامنه</th>
<th className="p-4 font-bold">وضعیت</th>
<th className="p-4 font-bold">اقدام</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{filteredItems.map((item) => (
<tr
key={item.id}
onClick={() => onSelectItem(item)}
className="hover:bg-slate-50 cursor-pointer transition-colors"
>
<td className="p-4 font-extrabold text-slate-900 max-w-xs">{item.title}</td>
<td className="p-4">
<span
className="px-2.5 py-0.5 rounded-full text-[10.5px] font-bold"
style={{
backgroundColor: `${getCategoryColor(item.category)}15`,
color: getCategoryColor(item.category)
}}
>
{getCategoryLabelFa(item.category)}
</span>
</td>
<td className="p-4 text-slate-600">{getHorizonLabelFa(item.horizon)}</td>
<td className="p-4 font-bold text-teal-600 font-mono">{toPersianDigits(item.impactScore)}/۵</td>
<td className="p-4 text-slate-500 font-mono">{toPersianDigits(item.uncertaintyScore)}/۵</td>
<td className="p-4 text-slate-600">{item.geographicScope}</td>
<td className="p-4">
<span className="badge badge-gray">{item.status}</span>
</td>
<td className="p-4">
<button className="text-teal-600 font-bold hover:underline">
مشاهده پرونده
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
};

202
src/pages/ReportsPage.tsx Normal file
View File

@ -0,0 +1,202 @@
import React from 'react';
import { IntelligenceItem, University1415Dimension, JobEntity } from '../types/futures';
import { toPersianDigits, getCategoryLabelFa, getHorizonLabelFa } from '../utils/persianNumbers';
import { Printer, Download, FileText, CheckCircle2, AlertTriangle, Target, Compass } from 'lucide-react';
interface ReportsPageProps {
items: IntelligenceItem[];
dimensions: University1415Dimension[];
jobs: JobEntity[];
}
export const ReportsPage: React.FC<ReportsPageProps> = ({ items, dimensions, jobs }) => {
const handlePrint = () => {
window.print();
};
const criticalItems = items.filter((i) => i.status === 'critical' || i.impactScore === 5);
const sortedGaps = [...dimensions].sort((a, b) => b.gap - a.gap);
const avgReadiness = Math.round(
dimensions.reduce((acc, d) => acc + d.readinessPercentage, 0) / dimensions.length
);
return (
<div className="flex flex-col gap-6 max-w-5xl mx-auto">
{/* Action Header (Hidden in Print) */}
<div className="no-print flex flex-col md:flex-row md:items-center justify-between gap-4 pb-2 border-b border-slate-800">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="badge badge-teal">مرکز هوشمندی راهبردی</span>
<span className="text-slate-400 text-xs">گزارش مدیریتی و نهادی</span>
</div>
<h1 className="page-title text-2xl text-white">
گزارشهای راهبردی و بولتنهای آیندهپژوهی
</h1>
<p className="page-subtitle text-slate-300 text-xs mt-1">
نسخه آماده چاپ و خروجی رسمی ویژه اعضای هیئت رئیسه و شورای دانشگاه اصفهان
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={handlePrint}
className="btn btn-primary text-xs py-2 px-4 flex items-center gap-1.5 shadow-lg"
>
<Printer size={15} />
<span>چاپ گزارش راهبردی (PDF)</span>
</button>
</div>
</div>
{/* Printable Institutional Report Document */}
<div className="intel-card p-8 lg:p-12 flex flex-col gap-8 bg-[#0c162b] border-[#1c2c4d] shadow-2xl">
{/* Document Header */}
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 pb-6 border-b-2 border-teal-500/40">
<div className="flex items-center gap-3.5">
<div className="w-12 h-12 rounded-xl bg-[#091122] border border-teal-400 flex items-center justify-center text-teal-400 font-bold">
<Compass size={28} />
</div>
<div>
<h2 className="text-white font-black text-xl lg:text-2xl">
دانشگاه اصفهان رادار آیندهپژوهی و هوشمندی راهبردی
</h2>
<div className="text-slate-400 text-xs mt-1">
بولتن تحلیلی شماره ۱۲ | افق راهبردی دانشگاه در سال ۱۴۱۵
</div>
</div>
</div>
<div className="text-left text-xs text-slate-400 flex flex-col">
<span className="font-bold text-white">طبقه‌بندی: محرمانه راهبردی</span>
<span>تاریخ انتشار: پاییز ۱۴۰۳</span>
<span className="font-mono text-teal-400">UI-FORESIGHT-1415</span>
</div>
</div>
{/* Executive Summary Narrative */}
<div className="flex flex-col gap-3 text-xs leading-relaxed text-slate-200 bg-[#091122] p-5 rounded-xl border border-slate-800">
<h3 className="text-teal-300 font-extrabold text-sm flex items-center gap-2">
<FileText size={16} />
<span>چکیده اجرایی ویژه ریاست محترم و هیئت رئیسه دانشگاه اصفهان</span>
</h3>
<p>
این گزارش تحلیلی حاصل پایش مستمر{' '}
<strong className="text-white">{toPersianDigits(items.length)}</strong> پیشران، فناوری، سیاست و سیگنال ضعیف آینده توسط مرکز هوشمندی دانشگاه اصفهان است. ارزیابی جامع ۱۰ کلانویژگی افق ۱۴۱۵ نشان میدهد که شاخص کل آمادگی دانشگاه در حال حاضر{' '}
<strong className="text-teal-400 font-mono font-bold">
{toPersianDigits(avgReadiness)}٪
</strong>{' '}
بوده و فاصله معناداری با وضعیت مطلوب دارد.
</p>
<p>
بزرگترین شکافهای عملکردی در بعد{' '}
<strong className="text-rose-400 font-bold">«هوشمصنوعیبنیان» (شکاف ۵.۳ نمره)</strong> و{' '}
<strong className="text-amber-400 font-bold">«انعطافپذیر و چابک» (شکاف ۵.۴ نمره)</strong> متمرکز است که به دلیل حفظ الگوهای سنتی امتحانات و دیوانسالاری اداری ایجاد شده است. در نقطه مقابل، بعد «انسانمحور» و «بینرشتهای» با ۶۰٪ آمادگی به عنوان مزیت رقابتی دانشگاه شناخته میشوند.
</p>
</div>
{/* High Priority Critical Items Table */}
<div className="flex flex-col gap-3 text-xs">
<h3 className="text-rose-400 font-extrabold text-sm flex items-center gap-2">
<AlertTriangle size={16} />
<span>جدول پیشرانهای با فوریت بحرانی و نیازمند واکنش فوری</span>
</h3>
<div className="overflow-x-auto">
<table className="w-full text-right text-xs border border-slate-800 rounded-lg overflow-hidden">
<thead className="bg-[#091122] text-slate-400">
<tr>
<th className="p-3 border-b border-slate-800">عنوان پدیده / سیگنال</th>
<th className="p-3 border-b border-slate-800">رده</th>
<th className="p-3 border-b border-slate-800">افق زمانی</th>
<th className="p-3 border-b border-slate-800">ضریب اثر</th>
<th className="p-3 border-b border-slate-800">مهمترین پیامد برای دانشگاه</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/80">
{criticalItems.map((c) => (
<tr key={c.id}>
<td className="p-3 font-bold text-white">{c.title}</td>
<td className="p-3 text-slate-300">{getCategoryLabelFa(c.category)}</td>
<td className="p-3 text-slate-400">{getHorizonLabelFa(c.horizon)}</td>
<td className="p-3 text-rose-400 font-bold font-mono">{toPersianDigits(c.impactScore)}/۵</td>
<td className="p-3 text-slate-300 max-w-sm">{c.whyItMatters}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* 1415 Readiness & Gap Matrix */}
<div className="flex flex-col gap-3 text-xs">
<h3 className="text-teal-400 font-extrabold text-sm flex items-center gap-2">
<Target size={16} />
<span>ماتریس شکافهای راهبردی دانشگاه اصفهان در افق ۱۴۱۵</span>
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{sortedGaps.map((dim) => (
<div
key={dim.id}
className="bg-[#091122] p-3 rounded-xl border border-slate-800 flex flex-col gap-1.5"
>
<div className="flex items-center justify-between">
<span className="font-bold text-white">{dim.title}</span>
<span className="text-rose-400 font-bold bg-rose-500/10 px-2 py-0.5 rounded text-[11px]">
شکاف: {toPersianDigits(dim.gap)} نمره
</span>
</div>
<div className="flex items-center justify-between text-slate-400 text-[11px]">
<span>وضع موجود: {toPersianDigits(dim.currentScore)}/۱۰</span>
<span>هدف ۱۴۱۵: {toPersianDigits(dim.targetScore)}/۱۰</span>
<span className="text-teal-300 font-semibold">{toPersianDigits(dim.readinessPercentage)}٪ آمادگی</span>
</div>
<div className="text-slate-300 text-[11px] mt-1 border-t border-slate-800 pt-1">
<strong>اقدام کلیدی:</strong> {dim.strategicRecommendations[0]}
</div>
</div>
))}
</div>
</div>
{/* Actionable Directives for Academic Leadership */}
<div className="flex flex-col gap-3 text-xs bg-gradient-to-br from-[#0b172e] to-[#091d24] p-5 rounded-xl border border-teal-500/30">
<h3 className="text-teal-300 font-black text-sm flex items-center gap-2">
<CheckCircle2 size={16} />
<span>پنج فرمان راهبردی هیئت رئیسه برای سال تحصیلی پیشرو</span>
</h3>
<ol className="list-decimal list-inside text-slate-200 space-y-2 text-[11.5px] leading-relaxed">
<li>
<strong>تأسیس مرکز هوش مصنوعی شناختی و تحول آموزش:</strong> بازطراحی شیوههای سنجش بدون اتکا به آزمون کتبی سنتی در ۳۰ گروه آموزشی.
</li>
<li>
<strong>ابلاغ طرح مأموریتهای استانی زایندهرود و فرونشست:</strong> اختصاص حداقل ۵۰٪ پژوهانههای دانشگاه به راهحلهای علمی بحران آب و خاک اصفهان.
</li>
<li>
<strong>راهاندازی دورههای صنعتی Co-op با شرکتهای برتر استان:</strong> تضمین بورسیه و حقوق ماهیانه برای دانشجویان کارشناسی در صنایع فولاد و هایتک.
</li>
<li>
<strong>توسعه پلتفرم دیپلماسی علمی منطقهای:</strong> ارتقای خوابگاهها و خدمات زبان فارسی برای هدفگذاری جذب ۳۰۰۰ دانشجوی خارجی.
</li>
<li>
<strong>مقرراتزدایی آموزشی و استقرار دورههای ریزمدرک:</strong> تفویض اختیار تغییر ۴۰٪ سرفصلها به شوراهای دانشکدهها.
</li>
</ol>
</div>
{/* Signature Box */}
<div className="pt-6 border-t border-slate-800 flex items-center justify-between text-xs text-slate-400">
<div>
<span>تأییدیه مرکز دیدهبانی و هوشمندی راهبردی</span>
<div className="font-bold text-white mt-1">دکتر مریم میرمحمدی / مدیر پروژه رادار آینده</div>
</div>
<div className="text-left">
<span>ملاحظه و ابلاغ ریاست دانشگاه اصفهان</span>
<div className="font-bold text-teal-400 mt-1">دکتر حسین هرسیج / رئیس دانشگاه</div>
</div>
</div>
</div>
</div>
);
};

149
src/pages/SettingsPage.tsx Normal file
View File

@ -0,0 +1,149 @@
import React, { useState } from 'react';
import { dataService } from '../services/dataService';
import { Settings, Download, Upload, RotateCcw, Shield, CheckCircle2, Moon, Sun } from 'lucide-react';
interface SettingsPageProps {
onRefreshData: () => void;
}
export const SettingsPage: React.FC<SettingsPageProps> = ({ onRefreshData }) => {
const [successMsg, setSuccessMsg] = useState('');
const [jsonInput, setJsonInput] = useState('');
const handleExport = () => {
const dataStr = dataService.exportJson();
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `ui_futures_radar_backup_${new Date().toISOString().slice(0, 10)}.json`;
link.click();
showSuccess('فایل پشتیبان JSON با موفقیت ذخیره شد');
};
const handleImport = () => {
if (!jsonInput.trim()) return;
const success = dataService.importJson(jsonInput);
if (success) {
onRefreshData();
showSuccess('داده‌ها با موفقیت بازیابی شدند');
setJsonInput('');
} else {
alert('ساختار فایل JSON نامعتبر است');
}
};
const handleReset = () => {
if (window.confirm('آیا از بازنشانی داده‌ها به تنظیمات پیش‌فرض سازمانی دانشگاه اصفهان اطمینان دارید؟')) {
dataService.resetToDefault();
onRefreshData();
showSuccess('تنظیمات و داده‌های سامانه به وضعیت اولیه بازگردانی شد');
}
};
const showSuccess = (msg: string) => {
setSuccessMsg(msg);
setTimeout(() => setSuccessMsg(''), 3000);
};
return (
<div className="flex flex-col gap-6 max-w-4xl mx-auto">
{/* Header */}
<div className="pb-2 border-b border-slate-800">
<h1 className="page-title text-2xl text-white">
تنظیمات سامانه و مدیریت پشتیبان (Platform Settings)
</h1>
<p className="page-subtitle text-slate-300 text-xs mt-1">
پیکربندی محیط، پشتیبانگیری از دادههای آیندهپژوهی، بازیابی اطلاعات و مدیریت نشست
</p>
</div>
{successMsg && (
<div className="p-3 bg-teal-950/40 border border-teal-500/50 rounded-xl text-teal-300 text-xs flex items-center gap-2 animate-fadeIn">
<CheckCircle2 size={16} className="text-teal-400" />
<span>{successMsg}</span>
</div>
)}
{/* Institutional System Info */}
<div className="intel-card flex flex-col gap-3">
<h3 className="text-white font-bold text-sm border-b border-slate-800 pb-2">
مشخصات استقرار سامانه سازمانی
</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 text-xs">
<div className="bg-[#091122] p-3 rounded-lg border border-slate-800">
<span className="text-slate-400 text-[11px] block">نام سامانه:</span>
<span className="font-bold text-white">رادار آینده دانشگاه اصفهان (UFR)</span>
</div>
<div className="bg-[#091122] p-3 rounded-lg border border-slate-800">
<span className="text-slate-400 text-[11px] block">افق چشمانداز:</span>
<span className="font-bold text-teal-400 font-mono">سال ۱۴۱۵ (۱۰ ساله)</span>
</div>
<div className="bg-[#091122] p-3 rounded-lg border border-slate-800">
<span className="text-slate-400 text-[11px] block">مالکیت فکری و سازمانی:</span>
<span className="font-bold text-white">معاونت پژوهش و فناوری دانشگاه اصفهان</span>
</div>
</div>
</div>
{/* Backup & Export / Import */}
<div className="intel-card flex flex-col gap-4">
<h3 className="text-white font-bold text-sm border-b border-slate-800 pb-2 flex items-center gap-2">
<Download size={16} className="text-teal-400" />
<span>پشتیبانگیری و بارگذاری دادهها (JSON Export / Import)</span>
</h3>
<p className="text-slate-300 text-xs leading-relaxed">
شما میتوانید از کلیه موجودیتهای رادار، نمرات کالیبرهشده ابعاد ۱۴۱۵ و تحلیلها یک نسخه کامل متنی با ساختار JSON دریافت کرده و در صورت نیاز بر روی سرور دیگری بازیابی فرمایید.
</p>
<div className="flex items-center gap-3">
<button
onClick={handleExport}
className="btn btn-primary text-xs py-2 px-4 flex items-center gap-2"
>
<Download size={15} />
<span>دریافت خروجی کامل JSON (Export)</span>
</button>
</div>
{/* JSON Import Box */}
<div className="mt-2 pt-3 border-t border-slate-800/80 flex flex-col gap-2">
<label className="text-slate-300 text-xs font-semibold">بارگذاری و بازیابی از فایل JSON:</label>
<textarea
rows={3}
value={jsonInput}
onChange={(e) => setJsonInput(e.target.value)}
placeholder="محتوای فایل JSON خروجی را اینجا الصاق کنید..."
className="w-full bg-[#081020] border border-slate-800 rounded-lg p-2.5 text-white font-mono text-xs outline-none focus:border-teal-500"
/>
<button
onClick={handleImport}
disabled={!jsonInput.trim()}
className="btn btn-secondary text-xs py-2 px-4 self-end flex items-center gap-1.5 disabled:opacity-40"
>
<Upload size={14} />
<span>بارگذاری و اعمال دادهها</span>
</button>
</div>
</div>
{/* Danger Zone: Reset to Baseline */}
<div className="intel-card border-rose-500/30 flex flex-col gap-3">
<h3 className="text-rose-400 font-bold text-sm flex items-center gap-2">
<RotateCcw size={16} />
<span>بازنشانی به تنظیمات کارخانه (Factory Reset)</span>
</h3>
<p className="text-slate-300 text-xs leading-relaxed">
با انتخاب این گزینه، تمامی تغییرات دستی و موارد اضافه شده پاک شده و پایگاه هوشمندی با بیش از ۱۰۰ سیگنال، روند و فناوری معیار دانشگاه اصفهان دوباره مقداردهی خواهد شد.
</p>
<button
onClick={handleReset}
className="btn btn-secondary text-xs py-2 px-4 text-rose-300 hover:bg-rose-950/40 border-rose-500/40 self-start"
>
بازنشانی کامل به دادههای معیار سازمانی
</button>
</div>
</div>
);
};

View File

@ -0,0 +1,78 @@
import React from 'react';
import { IntelligenceItem, University1415Dimension, JobEntity, SkillEntity } from '../types/futures';
import { StrategicGraph } from '../components/visual/StrategicGraph';
import { GitFork, Layers, Compass, ArrowLeft } from 'lucide-react';
interface StrategicNetworkPageProps {
items: IntelligenceItem[];
dimensions: University1415Dimension[];
jobs: JobEntity[];
skills: SkillEntity[];
onSelectItem: (item: IntelligenceItem) => void;
onSelectDimension: (dim: University1415Dimension) => void;
}
export const StrategicNetworkPage: React.FC<StrategicNetworkPageProps> = ({
items,
dimensions,
jobs,
skills,
onSelectItem,
onSelectDimension
}) => {
const handleSelectEntity = (type: string, id: string) => {
if (type === 'item') {
const it = items.find((i) => i.id === id);
if (it) onSelectItem(it);
} else if (type === 'dimension') {
const dim = dimensions.find((d) => d.id === id);
if (dim) onSelectDimension(dim);
}
};
return (
<div className="flex flex-col gap-6">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-2 border-b border-slate-800">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="badge badge-teal">تحلیل سیستمی و سناریوسازی</span>
<span className="text-slate-400 text-xs">نگاشت شبکه علّی اثرات</span>
</div>
<h1 className="page-title text-2xl text-white">
شبکه روابط و زنجیره ارزش تحول راهبردی (Strategic Impact Network)
</h1>
<p className="page-subtitle text-slate-300 text-xs mt-1">
ردگیری مسیر علّی و همبستگی پدیده‌ها: پیشران‌های بیرونی اختلال در آموزش عالی مهارتها مشاغل ابعاد ۱۴۱۵ دانشگاه اصفهان
</p>
</div>
</div>
{/* Conceptual Network Principle */}
<div className="intel-card border-teal-500/30 p-4 bg-[#0a1428] flex flex-col md:flex-row items-center justify-between gap-4 text-xs">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-teal-500/20 text-teal-400 flex items-center justify-center shrink-0">
<Compass size={20} />
</div>
<div>
<span className="font-bold text-white block text-sm">
اصل اساسی: رادار آینده دانشگاه یک گردآورنده اخبار ساده نیست!
</span>
<p className="text-slate-300 leading-relaxed mt-0.5">
ارزش افزوده این سامانه در اتصال نقاط کور است: هر فناوری (مانند هوش مصنوعی زاینده) مستقیماً به ضرورت تحول ارزشیابی، دگرگونی مهارتها، ظهور مشاغل نو و شکاف آمادگی بعد AI-Native دانشگاه اصفهان متصل است.
</p>
</div>
</div>
</div>
{/* Strategic Graph Component */}
<StrategicGraph
items={items}
dimensions={dimensions}
jobs={jobs}
skills={skills}
onSelectEntity={handleSelectEntity}
/>
</div>
);
};

View File

@ -0,0 +1,307 @@
import React, { useState } from 'react';
import { University1415Dimension, IntelligenceItem } from '../types/futures';
import { Dimension1415Card } from '../components/cards/Dimension1415Card';
import { dataService } from '../services/dataService';
import { toPersianDigits } from '../utils/persianNumbers';
import {
Target,
ArrowLeft,
X,
Lightbulb,
CheckCircle2,
TrendingUp,
Cpu,
BookOpen,
Sliders,
Save
} from 'lucide-react';
interface University1415PageProps {
dimensions: University1415Dimension[];
items: IntelligenceItem[];
onSelectItem: (item: IntelligenceItem) => void;
onRefreshDimensions: () => void;
}
export const University1415Page: React.FC<University1415PageProps> = ({
dimensions,
items,
onSelectItem,
onRefreshDimensions
}) => {
const [selectedDimension, setSelectedDimension] = useState<University1415Dimension | null>(null);
const [isEditingScores, setIsEditingScores] = useState(false);
const [editCurrent, setEditCurrent] = useState<number>(0);
const [editTarget, setEditTarget] = useState<number>(0);
const handleOpenDetail = (dim: University1415Dimension) => {
setSelectedDimension(dim);
setEditCurrent(dim.currentScore);
setEditTarget(dim.targetScore);
setIsEditingScores(false);
};
const handleSaveScores = () => {
if (!selectedDimension) return;
dataService.updateDimensionScores(selectedDimension.id, editCurrent, editTarget);
onRefreshDimensions();
const updated = dataService.getDimensionById(selectedDimension.id);
if (updated) setSelectedDimension(updated);
setIsEditingScores(false);
};
// Find linked items for selected dimension
const linkedTrends = selectedDimension
? items.filter((i) => selectedDimension.relatedTrendIds?.includes(i.id))
: [];
const linkedTech = selectedDimension
? items.filter((i) => selectedDimension.relatedTechIds?.includes(i.id))
: [];
const linkedPolicies = selectedDimension
? items.filter((i) => selectedDimension.relatedPolicyIds?.includes(i.id))
: [];
return (
<div className="flex flex-col gap-6">
{/* Page Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-2 border-b border-slate-800">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="badge badge-teal">سند راهبردی چشمانداز دانشگاه آینده</span>
<span className="text-slate-400 text-xs">افق دهساله ۱۴۱۵</span>
</div>
<h1 className="page-title text-2xl lg:text-3xl text-white">
دانشگاه اصفهان در سال ۱۴۱۵ (University 1415)
</h1>
<p className="page-subtitle text-slate-300 text-xs mt-1">
ده کلانویژگی، قابلیتهای بنیادین، وضعیت موجود، نمره مطلوب و اقدامات راهبردی تحول دانشگاه
</p>
</div>
<div className="flex items-center gap-3">
<div className="bg-[#0c1833] border border-teal-500/30 rounded-xl px-4 py-2 text-right">
<span className="text-[11px] text-slate-400 block">میانگین آمادگی سازمانی:</span>
<span className="text-lg font-black text-teal-400 font-mono">
{toPersianDigits(
Math.round(
dimensions.reduce((acc, d) => acc + d.readinessPercentage, 0) / dimensions.length
)
)}
٪
</span>
</div>
</div>
</div>
{/* 10 Dimension Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4">
{dimensions.map((dim) => (
<Dimension1415Card
key={dim.id}
dimension={dim}
onClick={() => handleOpenDetail(dim)}
/>
))}
</div>
{/* Detailed Drilldown Modal for Selected Dimension */}
{selectedDimension && (
<div className="modal-overlay" onClick={() => setSelectedDimension(null)}>
<div
className="modal-content max-w-3xl p-6 flex flex-col gap-5 text-right animate-fadeIn"
onClick={(e) => e.stopPropagation()}
style={{ borderTop: `4px solid ${selectedDimension.pillarColor}` }}
>
{/* Modal Header */}
<div className="flex items-start justify-between pb-3 border-b border-slate-800">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-mono text-slate-400">
{selectedDimension.titleEn}
</span>
<span
className="text-[11px] font-bold px-2 py-0.5 rounded-full"
style={{
backgroundColor: `${selectedDimension.pillarColor}20`,
color: selectedDimension.pillarColor
}}
>
آمادگی: {toPersianDigits(selectedDimension.readinessPercentage)}٪
</span>
</div>
<h2 className="text-white font-black text-xl">
{selectedDimension.title}
</h2>
</div>
<button
onClick={() => setSelectedDimension(null)}
className="p-1 text-slate-400 hover:text-white rounded-lg"
>
<X size={20} />
</button>
</div>
{/* Vision & Definition */}
<div className="flex flex-col gap-3 text-xs">
<div className="bg-[#091122] p-4 rounded-xl border border-slate-800">
<span className="text-teal-400 font-bold block mb-1">چشمانداز و تعریف (Definition & Vision):</span>
<p className="text-white font-semibold leading-relaxed mb-2">
{selectedDimension.vision}
</p>
<p className="text-slate-300 leading-relaxed">
{selectedDimension.definition}
</p>
</div>
{/* Score Assessment & Gap Calibration */}
<div className="bg-[#0c162d] p-4 rounded-xl border border-slate-800 flex flex-col gap-3">
<div className="flex items-center justify-between">
<span className="text-white font-bold">ارزیابی نمرات و شکاف راهبردی (Gap Analysis):</span>
<button
onClick={() => setIsEditingScores(!isEditingScores)}
className="text-teal-400 hover:underline text-[11px] font-semibold"
>
{isEditingScores ? 'لغو کالیبراسیون' : 'کالیبراسیون نمرات سازمانی'}
</button>
</div>
{!isEditingScores ? (
<div className="grid grid-cols-3 gap-3 text-center">
<div className="bg-[#091122] p-3 rounded-lg border border-slate-800">
<span className="text-slate-400 text-[11px] block">وضع موجود:</span>
<span className="text-amber-400 font-black text-base">
{toPersianDigits(selectedDimension.currentScore)} از ۱۰
</span>
</div>
<div className="bg-[#091122] p-3 rounded-lg border border-slate-800">
<span className="text-slate-400 text-[11px] block">هدف افق ۱۴۱۵:</span>
<span className="text-teal-400 font-black text-base">
{toPersianDigits(selectedDimension.targetScore)} از ۱۰
</span>
</div>
<div className="bg-[#091122] p-3 rounded-lg border border-rose-500/30">
<span className="text-slate-400 text-[11px] block">شکاف راهبردی:</span>
<span className="text-rose-400 font-black text-base">
{toPersianDigits(selectedDimension.gap)} نمره
</span>
</div>
</div>
) : (
<div className="bg-[#091122] p-4 rounded-lg border border-teal-500/40 flex flex-col gap-3">
<div className="flex items-center justify-between gap-4">
<div className="flex-1">
<label className="text-slate-300 block mb-1">وضع موجود (۱ تا ۱۰):</label>
<input
type="number"
step={0.1}
min={1}
max={10}
value={editCurrent}
onChange={(e) => setEditCurrent(Number(e.target.value))}
className="w-full bg-[#081020] border border-slate-700 rounded p-2 text-white text-xs"
/>
</div>
<div className="flex-1">
<label className="text-slate-300 block mb-1">هدف ۱۴۱۵ (۱ تا ۱۰):</label>
<input
type="number"
step={0.1}
min={1}
max={10}
value={editTarget}
onChange={(e) => setEditTarget(Number(e.target.value))}
className="w-full bg-[#081020] border border-slate-700 rounded p-2 text-white text-xs"
/>
</div>
</div>
<button
onClick={handleSaveScores}
className="btn btn-primary text-xs py-2 self-end flex items-center gap-1"
>
<Save size={13} />
<span>ثبت و بازنگری شکاف</span>
</button>
</div>
)}
</div>
{/* Supporting Evidence in University of Isfahan */}
<div className="bg-[#091122] p-4 rounded-xl border border-slate-800">
<span className="text-purple-400 font-bold block mb-2 flex items-center gap-1.5">
<CheckCircle2 size={15} />
شواهد و اقدامات در حال اجرا در دانشگاه اصفهان:
</span>
<ul className="list-disc list-inside text-slate-300 space-y-1.5 leading-relaxed text-[11.5px]">
{selectedDimension.supportingEvidence.map((ev, idx) => (
<li key={idx}>{ev}</li>
))}
</ul>
</div>
{/* Strategic Recommendations */}
<div className="bg-[#091122] p-4 rounded-xl border border-teal-500/30 bg-teal-950/10">
<span className="text-teal-400 font-bold block mb-2 flex items-center gap-1.5">
<Lightbulb size={15} />
اقدامات راهبردی اولویتدار برای بستن شکاف (Actionable Roadmap):
</span>
<ol className="list-decimal list-inside text-slate-200 space-y-1.5 leading-relaxed text-[11.5px]">
{selectedDimension.strategicRecommendations.map((rec, idx) => (
<li key={idx}>{rec}</li>
))}
</ol>
</div>
{/* Traceability: Linked Trends, Tech, Policies */}
<div className="pt-2 border-t border-slate-800 flex flex-col gap-3">
<span className="text-slate-400 font-bold text-xs">پیشرانها و فناوریهای مرتبط در رادار:</span>
<div className="flex flex-wrap gap-2">
{linkedTrends.map((t) => (
<button
key={t.id}
onClick={() => {
setSelectedDimension(null);
onSelectItem(t);
}}
className="bg-[#0b162e] hover:bg-[#122244] border border-sky-500/30 text-sky-300 px-3 py-1.5 rounded-lg text-xs flex items-center gap-1"
>
<TrendingUp size={12} />
<span>{t.title}</span>
</button>
))}
{linkedTech.map((tc) => (
<button
key={tc.id}
onClick={() => {
setSelectedDimension(null);
onSelectItem(tc);
}}
className="bg-[#0b162e] hover:bg-[#122244] border border-teal-500/30 text-teal-300 px-3 py-1.5 rounded-lg text-xs flex items-center gap-1"
>
<Cpu size={12} />
<span>{tc.title}</span>
</button>
))}
{linkedPolicies.map((pol) => (
<button
key={pol.id}
onClick={() => {
setSelectedDimension(null);
onSelectItem(pol);
}}
className="bg-[#0b162e] hover:bg-[#122244] border border-amber-500/30 text-amber-300 px-3 py-1.5 rounded-lg text-xs flex items-center gap-1"
>
<BookOpen size={12} />
<span>{pol.title}</span>
</button>
))}
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
};

178
src/services/authService.ts Normal file
View File

@ -0,0 +1,178 @@
import { UserSession, PersonaType } from '../types/futures';
const AUTH_STORAGE_KEY = 'ufr_auth_session_v1';
export const PRESET_USERS: Record<PersonaType, UserSession> = {
president: {
id: 'usr-1',
username: 'president',
fullName: 'دکتر حسین هرسیج',
role: 'president',
roleTitleFa: 'رئیس دانشگاه اصفهان',
department: 'ریاست و هیئت رئیسه',
token: 'jwt-pres-778899',
avatarUrl: ''
},
researcher: {
id: 'usr-2',
username: 'researcher',
fullName: 'دکتر مریم میرمحمدی',
role: 'researcher',
roleTitleFa: 'پژوهشگر ارشد رصد آینده و سیگنال‌شناسی',
department: 'پژوهشکده مطالعات آینده و علوم شناختی',
token: 'jwt-res-334455',
avatarUrl: ''
},
planner: {
id: 'usr-3',
username: 'planner',
fullName: 'دکتر علیرضا فروغی',
role: 'planner',
roleTitleFa: 'مدیر کل برنامه‌ریزی راهبردی و تحول',
department: 'معاونت طرح، برنامه و تحول آموزشی',
token: 'jwt-plan-112233',
avatarUrl: ''
},
admin: {
id: 'usr-4',
username: 'admin',
fullName: 'مهندس سعید فیروزبخش',
role: 'admin',
roleTitleFa: 'راهبر ارشد سامانه رادار هوشمندی',
department: 'مرکز فناوری اطلاعات و آمار دانشگاه',
token: 'jwt-adm-990011',
avatarUrl: ''
}
};
class AuthService {
private currentSession: UserSession | null = null;
private listeners: ((session: UserSession | null) => void)[] = [];
constructor() {
this.loadSession();
}
private loadSession() {
try {
const stored = localStorage.getItem(AUTH_STORAGE_KEY);
if (stored) {
this.currentSession = JSON.parse(stored);
} else {
// Default authenticated persona for immediate institutional exploration
this.currentSession = PRESET_USERS.president;
this.saveSession();
}
} catch (e) {
console.warn(e);
this.currentSession = PRESET_USERS.president;
}
}
private saveSession() {
try {
if (this.currentSession) {
localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(this.currentSession));
} else {
localStorage.removeItem(AUTH_STORAGE_KEY);
}
this.notifyListeners();
} catch (e) {
console.error(e);
}
}
public subscribe(cb: (session: UserSession | null) => void): () => void {
this.listeners.push(cb);
cb(this.currentSession);
return () => {
this.listeners = this.listeners.filter((l) => l !== cb);
};
}
private notifyListeners() {
this.listeners.forEach((cb) => cb(this.currentSession));
}
public getSession(): UserSession | null {
return this.currentSession;
}
public isAuthenticated(): boolean {
return !!this.currentSession;
}
public loginWithCredentials(username: string, _password: string): Promise<UserSession> {
return new Promise((resolve, reject) => {
setTimeout(() => {
const u = username.toLowerCase().trim();
let matched: UserSession | undefined;
if (u === 'president' || u.includes('هرسیج') || u === 'p') {
matched = PRESET_USERS.president;
} else if (u === 'researcher' || u.includes('میرمحمدی') || u === 'r') {
matched = PRESET_USERS.researcher;
} else if (u === 'planner' || u.includes('فروغی') || u === 's') {
matched = PRESET_USERS.planner;
} else if (u === 'admin' || u === 'a') {
matched = PRESET_USERS.admin;
} else {
// Allow any academic login default
matched = {
id: 'usr-' + Date.now(),
username: u,
fullName: 'کاربر دانشگاهی (' + u + ')',
role: 'planner',
roleTitleFa: 'تصمیم‌گیر دانشگاهی',
department: 'دانشگاه اصفهان',
token: 'jwt-dyn-' + Date.now()
};
}
if (matched) {
this.currentSession = matched;
this.saveSession();
resolve(matched);
} else {
reject(new Error('نام کاربری یا رمز عبور اشتباه است'));
}
}, 500);
});
}
public sendOtp(_mobileOrEmail: string): Promise<{ success: boolean; simulatedOtp: string }> {
return new Promise((resolve) => {
setTimeout(() => {
// Return simulated 5-digit code for testing convenience
resolve({ success: true, simulatedOtp: '14150' });
}, 600);
});
}
public verifyOtp(otpCode: string): Promise<UserSession> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (otpCode.trim() === '14150' || otpCode.trim().length >= 4) {
this.currentSession = PRESET_USERS.president;
this.saveSession();
resolve(PRESET_USERS.president);
} else {
reject(new Error('کد یکبار مصرف وارد شده نامعتبر یا منقضی شده است'));
}
}, 500);
});
}
public switchPersona(persona: PersonaType): UserSession {
this.currentSession = PRESET_USERS[persona];
this.saveSession();
return this.currentSession;
}
public logout(): void {
this.currentSession = null;
this.saveSession();
}
}
export const authService = new AuthService();

350
src/services/dataService.ts Normal file
View File

@ -0,0 +1,350 @@
import {
IntelligenceItem,
University1415Dimension,
JobEntity,
SkillEntity,
FuturesForce,
Category,
FilterState
} from '../types/futures';
import {
INITIAL_TRENDS,
INITIAL_TECHNOLOGIES,
INITIAL_POLICIES,
INITIAL_SIGNALS,
INITIAL_DIMENSIONS,
INITIAL_JOBS,
INITIAL_SKILLS,
INITIAL_FORCES
} from '../data/seedData';
const STORAGE_KEYS = {
ITEMS: 'ufr_intelligence_items_v1',
DIMENSIONS: 'ufr_dimensions_v1',
JOBS: 'ufr_jobs_v1',
SKILLS: 'ufr_skills_v1',
FORCES: 'ufr_forces_v1'
};
class DataService {
private items: IntelligenceItem[] = [];
private dimensions: University1415Dimension[] = [];
private jobs: JobEntity[] = [];
private skills: SkillEntity[] = [];
private forces: FuturesForce[] = [];
constructor() {
this.init();
}
private init() {
try {
const storedItems = localStorage.getItem(STORAGE_KEYS.ITEMS);
if (storedItems) {
this.items = JSON.parse(storedItems);
} else {
this.items = [
...INITIAL_TRENDS,
...INITIAL_TECHNOLOGIES,
...INITIAL_POLICIES,
...INITIAL_SIGNALS
];
this.saveItems();
}
const storedDims = localStorage.getItem(STORAGE_KEYS.DIMENSIONS);
if (storedDims) {
this.dimensions = JSON.parse(storedDims);
} else {
this.dimensions = [...INITIAL_DIMENSIONS];
this.saveDimensions();
}
const storedJobs = localStorage.getItem(STORAGE_KEYS.JOBS);
if (storedJobs) {
this.jobs = JSON.parse(storedJobs);
} else {
this.jobs = [...INITIAL_JOBS];
this.saveJobs();
}
const storedSkills = localStorage.getItem(STORAGE_KEYS.SKILLS);
if (storedSkills) {
this.skills = JSON.parse(storedSkills);
} else {
this.skills = [...INITIAL_SKILLS];
this.saveSkills();
}
const storedForces = localStorage.getItem(STORAGE_KEYS.FORCES);
if (storedForces) {
this.forces = JSON.parse(storedForces);
} else {
this.forces = [...INITIAL_FORCES];
this.saveForces();
}
} catch (e) {
console.warn('Error reading from localStorage, using in-memory seed data', e);
this.items = [
...INITIAL_TRENDS,
...INITIAL_TECHNOLOGIES,
...INITIAL_POLICIES,
...INITIAL_SIGNALS
];
this.dimensions = [...INITIAL_DIMENSIONS];
this.jobs = [...INITIAL_JOBS];
this.skills = [...INITIAL_SKILLS];
this.forces = [...INITIAL_FORCES];
}
}
private saveItems() {
try {
localStorage.setItem(STORAGE_KEYS.ITEMS, JSON.stringify(this.items));
} catch (e) {
console.error(e);
}
}
private saveDimensions() {
try {
localStorage.setItem(STORAGE_KEYS.DIMENSIONS, JSON.stringify(this.dimensions));
} catch (e) {
console.error(e);
}
}
private saveJobs() {
try {
localStorage.setItem(STORAGE_KEYS.JOBS, JSON.stringify(this.jobs));
} catch (e) {
console.error(e);
}
}
private saveSkills() {
try {
localStorage.setItem(STORAGE_KEYS.SKILLS, JSON.stringify(this.skills));
} catch (e) {
console.error(e);
}
}
private saveForces() {
try {
localStorage.setItem(STORAGE_KEYS.FORCES, JSON.stringify(this.forces));
} catch (e) {
console.error(e);
}
}
// Intelligence Items CRUD
public getAllItems(): IntelligenceItem[] {
return [...this.items];
}
public getItemById(id: string): IntelligenceItem | undefined {
return this.items.find((it) => it.id === id);
}
public getItemsByCategory(category: Category): IntelligenceItem[] {
return this.items.filter((it) => it.category === category);
}
public createItem(newItem: IntelligenceItem): IntelligenceItem {
this.items.unshift(newItem);
this.saveItems();
return newItem;
}
public updateItem(updated: IntelligenceItem): boolean {
const idx = this.items.findIndex((it) => it.id === updated.id);
if (idx !== -1) {
this.items[idx] = updated;
this.saveItems();
return true;
}
return false;
}
public deleteItem(id: string): boolean {
const prevLen = this.items.length;
this.items = this.items.filter((it) => it.id !== id);
if (this.items.length !== prevLen) {
this.saveItems();
return true;
}
return false;
}
// Dimensions
public getDimensions(): University1415Dimension[] {
return [...this.dimensions];
}
public getDimensionById(id: string): University1415Dimension | undefined {
return this.dimensions.find((d) => d.id === id);
}
public updateDimensionScores(id: string, currentScore: number, targetScore: number): boolean {
const dim = this.dimensions.find((d) => d.id === id);
if (dim) {
dim.currentScore = Number(currentScore.toFixed(1));
dim.targetScore = Number(targetScore.toFixed(1));
dim.gap = Number(Math.max(0, dim.targetScore - dim.currentScore).toFixed(1));
dim.readinessPercentage = Math.round((dim.currentScore / dim.targetScore) * 100);
this.saveDimensions();
return true;
}
return false;
}
// Jobs & Skills
public getJobs(): JobEntity[] {
return [...this.jobs];
}
public getSkills(): SkillEntity[] {
return [...this.skills];
}
public createJob(job: JobEntity): JobEntity {
this.jobs.unshift(job);
this.saveJobs();
return job;
}
public createSkill(skill: SkillEntity): SkillEntity {
this.skills.unshift(skill);
this.saveSkills();
return skill;
}
// Forces
public getFuturesForces(): FuturesForce[] {
return [...this.forces];
}
// Filtering
public filterItems(filters: FilterState): IntelligenceItem[] {
return this.items.filter((item) => {
if (filters.category && filters.category !== 'all' && item.category !== filters.category) {
return false;
}
if (filters.horizon && filters.horizon !== 'all' && item.horizon !== filters.horizon) {
return false;
}
if (filters.minImpact && item.impactScore < filters.minImpact) {
return false;
}
if (filters.status && filters.status !== 'all' && item.status !== filters.status) {
return false;
}
if (filters.searchQuery && filters.searchQuery.trim() !== '') {
const q = filters.searchQuery.toLowerCase().trim();
const inTitle = item.title.toLowerCase().includes(q);
const inSummary = item.executiveSummary.toLowerCase().includes(q);
const inWhy = item.whyItMatters.toLowerCase().includes(q);
const inRel = item.relevanceToUniversity.toLowerCase().includes(q);
if (!inTitle && !inSummary && !inWhy && !inRel) return false;
}
return true;
});
}
// Executive Foresight KPI Metrics
public getExecutiveKpis() {
const totalSignals = this.items.filter((i) => i.category === 'weak_signal').length;
const activeSignals = this.items.filter((i) => i.category === 'weak_signal' && (i.status === 'critical' || i.status === 'active')).length;
const emergingTrends = this.items.filter((i) => i.category === 'trend').length;
const criticalTech = this.items.filter((i) => i.category === 'technology' && i.impactScore >= 4).length;
const policyChanges = this.items.filter((i) => i.category === 'policy').length;
const highImpactLabor = this.jobs.filter((j) => Math.abs(j.growthRatePercent) >= 35).length;
// Average Readiness across 10 dimensions
const totalReadiness = this.dimensions.reduce((acc, d) => acc + d.readinessPercentage, 0);
const avgReadiness = Math.round(totalReadiness / (this.dimensions.length || 1));
// Sort largest gaps
const sortedByGap = [...this.dimensions].sort((a, b) => b.gap - a.gap);
const largestGap = sortedByGap[0];
const secondLargestGap = sortedByGap[1];
// Strongest dimension
const sortedByScore = [...this.dimensions].sort((a, b) => b.currentScore - a.currentScore);
const strongestDimension = sortedByScore[0];
return {
totalItems: this.items.length,
totalSignals,
activeSignals,
emergingTrends,
criticalTech,
policyChanges,
highImpactLabor,
avgReadiness,
largestGap,
secondLargestGap,
strongestDimension
};
}
// Backup & Reset
public resetToDefault() {
this.items = [
...INITIAL_TRENDS,
...INITIAL_TECHNOLOGIES,
...INITIAL_POLICIES,
...INITIAL_SIGNALS
];
this.dimensions = [...INITIAL_DIMENSIONS];
this.jobs = [...INITIAL_JOBS];
this.skills = [...INITIAL_SKILLS];
this.forces = [...INITIAL_FORCES];
this.saveItems();
this.saveDimensions();
this.saveJobs();
this.saveSkills();
this.saveForces();
}
public exportJson(): string {
return JSON.stringify(
{
items: this.items,
dimensions: this.dimensions,
jobs: this.jobs,
skills: this.skills,
forces: this.forces,
exportDate: new Date().toISOString(),
institution: 'University of Isfahan (دانشگاه اصفهان)'
},
null,
2
);
}
public importJson(jsonStr: string): boolean {
try {
const parsed = JSON.parse(jsonStr);
if (Array.isArray(parsed.items) && Array.isArray(parsed.dimensions)) {
this.items = parsed.items;
this.dimensions = parsed.dimensions;
if (Array.isArray(parsed.jobs)) this.jobs = parsed.jobs;
if (Array.isArray(parsed.skills)) this.skills = parsed.skills;
if (Array.isArray(parsed.forces)) this.forces = parsed.forces;
this.saveItems();
this.saveDimensions();
this.saveJobs();
this.saveSkills();
this.saveForces();
return true;
}
} catch (e) {
console.error('Import failed', e);
}
return false;
}
}
export const dataService = new DataService();

139
src/types/futures.ts Normal file
View File

@ -0,0 +1,139 @@
export type Horizon = 'now' | 'near' | 'mid' | 'long';
// 'now': افق اکنون (۱۴۰۳-۱۴۰۵) / 0-2 سال
// 'near': افق نزدیک (۱۴۰۵-۱۴۰۸) / 2-5 سال
// 'mid': افق میان‌مدت (۱۴۰۸-۱۴۱۱) / 5-8 سال
// 'long': افق بلندمدت (۱۴۱۱-۱۴۱۵) / 8-12 سال
export type Category = 'trend' | 'technology' | 'policy' | 'weak_signal';
export type Confidence = 'low' | 'medium' | 'high';
export type ForceType = 'pull' | 'push' | 'weight';
export type ItemStatus = 'active' | 'emerging' | 'monitoring' | 'critical';
export interface ImpactAssessment {
education: number; // تأثیر بر آموزش و برنامه‌های درسی (1-5)
research: number; // تأثیر بر پژوهش و تولید علم (1-5)
operations: number; // تأثیر بر مدیریت و فرآیندهای اداری/سازمانی (1-5)
students: number; // تأثیر بر تجربه، هویت و یادگیری دانشجویان (1-5)
faculty: number; // تأثیر بر اعضای هیئت علمی و اساتید (1-5)
laborMarket: number; // تأثیر بر اشتغال‌پذیری و تقاضای بازار کار (1-5)
strategicImportance: number;// اهمیت راهبردی برای دانشگاه اصفهان (1-5)
}
export interface UniversityImplications {
opportunities: string[];
risks: string[];
threats: string[];
requiredCapabilities: string[];
affectedDepartments: string[];
}
export interface EvidenceSource {
title: string;
source: string;
date: string;
url?: string;
snippet: string;
}
export interface IntelligenceItem {
id: string;
title: string;
category: Category;
executiveSummary: string;
whyItMatters: string;
horizon: Horizon;
impactScore: number; // 1 - 5
uncertaintyScore: number; // 1 - 5 (1 = قطعی، 5 = عدم قطعیت بالا)
confidence: Confidence;
geographicScope: 'ایران' | 'منطقه‌ای' | 'جهانی';
relevanceToUniversity: string;
status: ItemStatus;
impact: ImpactAssessment;
evidence: EvidenceSource[];
implications: UniversityImplications;
relatedItemIds: string[];
relatedJobIds: string[];
relatedSkillIds: string[];
relatedDimensionIds: string[];
}
export interface University1415Dimension {
id: string;
title: string;
titleEn: string;
shortTag: string;
vision: string;
definition: string;
currentScore: number; // 1 - 10
targetScore: number; // 1 - 10
gap: number; // targetScore - currentScore
readinessPercentage: number; // (currentScore / targetScore) * 100
supportingEvidence: string[];
relatedTrendIds: string[];
relatedTechIds: string[];
relatedPolicyIds: string[];
strategicRecommendations: string[];
pillarColor: string;
}
export interface JobEntity {
id: string;
title: string;
type: 'growing' | 'transforming';
growthRatePercent: number; // e.g. +42% or -28%
automationRiskPercent: number; // 0 - 100%
horizon: Horizon;
industry: string;
discipline: string; // دانشکده / گروه آموزشی مرتبط در دانشگاه اصفهان
description: string;
keyRequiredSkills: string[];
universityAction: string;
}
export interface SkillEntity {
id: string;
title: string;
type: 'growing' | 'declining';
category: 'digital' | 'cognitive' | 'social' | 'technical';
importanceScore: number; // 1 - 5
description: string;
affectedAcademicFields: string[];
replacementOrAugmentation: string;
}
export interface FuturesForce {
id: string;
title: string;
forceType: ForceType; // 'pull' | 'push' | 'weight'
category: string;
strength: number; // 1 - 10
evidence: string;
impact: string;
direction: string;
strategicImplication: string;
relatedSignalIds: string[];
}
export type PersonaType = 'president' | 'researcher' | 'planner' | 'admin';
export interface UserSession {
id: string;
username: string;
fullName: string;
role: PersonaType;
roleTitleFa: string;
department: string;
token: string;
avatarUrl?: string;
}
export interface FilterState {
category?: Category | 'all';
horizon?: Horizon | 'all';
searchQuery: string;
minImpact?: number;
confidence?: Confidence | 'all';
scope?: string;
department?: string;
status?: ItemStatus | 'all';
}

106
src/utils/persianNumbers.ts Normal file
View File

@ -0,0 +1,106 @@
import { Horizon, Category, ForceType, PersonaType } from '../types/futures';
export const toPersianDigits = (num: number | string | null | undefined): string => {
if (num === null || num === undefined) return '';
const str = String(num);
const persianDigits = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
return str.replace(/[0-9]/g, (w) => persianDigits[parseInt(w, 10)]);
};
export const getHorizonLabelFa = (horizon: Horizon): string => {
switch (horizon) {
case 'now':
return 'اکنون (۱۴۰۳-۱۴۰۵)';
case 'near':
return 'افق نزدیک (۱۴۰۵-۱۴۰۸)';
case 'mid':
return 'میان‌مدت (۱۴۰۸-۱۴۱۱)';
case 'long':
return 'بلندمدت (۱۴۱۱-۱۴۱۵)';
default:
return horizon;
}
};
export const getHorizonShortFa = (horizon: Horizon): string => {
switch (horizon) {
case 'now':
return 'اکنون';
case 'near':
return 'نزدیک';
case 'mid':
return 'میان‌مدت';
case 'long':
return 'بلندمدت';
default:
return horizon;
}
};
export const getCategoryLabelFa = (cat: Category): string => {
switch (cat) {
case 'trend':
return 'روندها (Trends)';
case 'technology':
return 'فناوری‌ها (Tech)';
case 'policy':
return 'سیاست‌ها (Policies)';
case 'weak_signal':
return 'سیگنال‌های آینده (Signals)';
default:
return cat;
}
};
export const getCategoryColor = (cat: Category): string => {
switch (cat) {
case 'trend':
return '#38bdf8'; // Blue/Sky
case 'technology':
return '#00d4aa'; // Teal/Emerald
case 'policy':
return '#fbbf24'; // Amber
case 'weak_signal':
return '#f43f5e'; // Rose/Coral
default:
return '#94a3b8';
}
};
export const getForceLabelFa = (type: ForceType): string => {
switch (type) {
case 'pull':
return 'کشش آینده (Pull)';
case 'push':
return 'فشارهای حال (Push)';
case 'weight':
return 'وزن گذشته (Weight)';
default:
return type;
}
};
export const getPersonaLabelFa = (p: PersonaType): { title: string; subtitle: string } => {
switch (p) {
case 'president':
return {
title: 'ریاست و هیئت رئیسه دانشگاه',
subtitle: 'دیدگاه راهبردی کلان و تصمیم‌گیری سازمانی'
};
case 'researcher':
return {
title: 'پژوهشگر آینده‌پژوهی و تحلیل‌گر علم',
subtitle: 'کاوش سیگنال‌های ضعیف و شواهد تجربی'
};
case 'planner':
return {
title: 'برنامه‌ریز راهبردی و تحول آموزشی',
subtitle: 'سنجش شکاف قابلیت‌ها و نقشه راه ۱۴۱۵'
};
case 'admin':
return {
title: 'مدیر ارشد سامانه هوشمندی',
subtitle: 'مدیریت داده‌ها و کالیبراسیون شاخص‌ها'
};
}
};

99
src/utils/radarMath.ts Normal file
View File

@ -0,0 +1,99 @@
import { Horizon, Category } from '../types/futures';
export interface RadarPoint {
x: number;
y: number;
r: number;
angle: number;
radius: number;
}
export const getHorizonRadiusRatio = (horizon: Horizon): number => {
switch (horizon) {
case 'now':
return 0.28; // حلقه اول: اکنون (۰-۲ سال)
case 'near':
return 0.52; // حلقه دوم: نزدیک (۲-۵ سال)
case 'mid':
return 0.74; // حلقه سوم: میان‌مدت (۵-۸ سال)
case 'long':
return 0.93; // حلقه چهارم: بلندمدت (۸-۱۲ سال)
default:
return 0.5;
}
};
export const getCategoryQuadrant = (category: Category): { startAngle: number; endAngle: number } => {
// Angle in radians (0 = 3 o'clock, PI/2 = 6 o'clock, PI = 9 o'clock, 3PI/2 = 12 o'clock)
// Top-Right: Trends (270° to 360° / -90° to 0°)
// Top-Left: Technologies (180° to 270°)
// Bottom-Left: Weak Signals (90° to 180°)
// Bottom-Right: Policies (0° to 90°)
switch (category) {
case 'trend':
return { startAngle: -Math.PI / 2 + 0.15, endAngle: -0.15 };
case 'technology':
return { startAngle: -Math.PI + 0.15, endAngle: -Math.PI / 2 - 0.15 };
case 'weak_signal':
return { startAngle: Math.PI / 2 + 0.15, endAngle: Math.PI - 0.15 };
case 'policy':
return { startAngle: 0.15, endAngle: Math.PI / 2 - 0.15 };
default:
return { startAngle: 0, endAngle: Math.PI / 2 };
}
};
export const calculateItemRadarCoords = (
id: string,
category: Category,
horizon: Horizon,
indexInGroup: number,
totalInGroup: number,
center: number,
maxRadius: number
): RadarPoint => {
const baseRatio = getHorizonRadiusRatio(horizon);
// deterministic pseudo-random offset based on string id
const hash = id.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
const radiusJitter = ((hash % 17) - 8) * 0.009;
const radius = maxRadius * Math.max(0.15, Math.min(0.98, baseRatio + radiusJitter));
const { startAngle, endAngle } = getCategoryQuadrant(category);
const span = endAngle - startAngle;
// Distribute items evenly in angular span with small deterministic spread
const step = totalInGroup > 1 ? span / (totalInGroup + 1) : span / 2;
const rawAngle = startAngle + step * (indexInGroup + 1);
const angleJitter = (((hash * 7) % 19) - 9) * 0.012;
const angle = rawAngle + angleJitter;
const x = center + radius * Math.cos(angle);
const y = center + radius * Math.sin(angle);
return { x, y, r: radius, angle, radius };
};
// Math for 10-axis Spider Chart
export const getSpiderCoordinates = (
values: number[], // 10 scores from 1 to 10
maxValue: number, // 10
center: number,
maxRadius: number
): { x: number; y: number }[] => {
const numAxes = values.length;
return values.map((val, idx) => {
// start from top (-PI / 2) and go clockwise
const angle = (2 * Math.PI * idx) / numAxes - Math.PI / 2;
const distance = (val / maxValue) * maxRadius;
const x = center + distance * Math.cos(angle);
const y = center + distance * Math.sin(angle);
return { x, y };
});
};
export const pointsToSvgPath = (points: { x: number; y: number }[]): string => {
if (points.length === 0) return '';
const first = points[0];
const rest = points.slice(1).map((p) => `L ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(' ');
return `M ${first.x.toFixed(1)} ${first.y.toFixed(1)} ${rest} Z`;
};

26
tsconfig.app.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"allowArbitraryExtensions": true,
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": false,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": false,
"noUnusedParameters": false,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

23
tsconfig.node.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"module": "nodenext",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})