feat: University Futures Radar — University of Isfahan foresight intelligence platform
This commit is contained in:
commit
c6fbb9c7f2
|
|
@ -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?
|
||||||
|
|
@ -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 }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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. **نگارش فارسی معیار و روان**:
|
||||||
|
- به کارگیری اصطلاحات دقیق آیندهپژوهی و مدیریت دانشگاهی (مانند: *سیگنالهای ضعیف، افق زمانی، شکاف بلوغ، شایستگیهای نوظهور، آمایش آموزشی*).
|
||||||
|
|
@ -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>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
|
|
@ -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 |
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -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 |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>,
|
||||||
|
)
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
|
@ -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: 'مدیریت دادهها و کالیبراسیون شاخصها'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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`;
|
||||||
|
};
|
||||||
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue