feat: create ideas and tech page

This commit is contained in:
mehrdad_adabi 2025-09-08 06:53:51 +03:30
parent e36fbf9874
commit cc163a19f0
4 changed files with 848 additions and 53 deletions

View File

@ -0,0 +1,794 @@
import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { Badge } from "~/components/ui/badge";
import { Card, CardContent } from "~/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "~/components/ui/table";
import apiService from "~/lib/api";
import { formatNumber } from "~/lib/utils";
import { DashboardLayout } from "../layout";
interface ProjectData {
WorkflowID: number;
ValueP1215S1887ValueID: number;
ValueP1215S1887StageID: number;
project_no: string;
importance_project: string;
title: string;
strategic_theme: string;
value_technology_and_innovation: string;
type_of_innovation: string;
innovation: string;
person_executing: string;
excellent_observer: string;
observer: string;
moderator: string;
start_date: string;
end_date: string | null;
done_date: string | null;
approved_budget: string;
budget_spent: string;
}
interface SortConfig {
field: string; // uses column.key
direction: "asc" | "desc";
}
type ColumnDef = {
key: string; // UI key
label: string;
sortable: boolean;
width: string;
apiField?: string; // API field name; defaults to key
computed?: boolean; // not fetched from API
};
// const columns: ColumnDef[] = [
// { key: "idea_title", label: "عنوان پروژه", sortable: true, width: "200px" },
// {
// key: "idea_status",
// label: "میزان اهمیت",
// sortable: true,
// width: "150px",
// },
// {
// key: "strategic_theme",
// label: "مضمون راهبردی",
// sortable: true,
// width: "160px",
// },
// {
// key: "value_technology_and_innovation",
// label: "ارزش فناوری و نوآوری",
// sortable: true,
// width: "200px",
// },
// {
// key: "type_of_innovation",
// label: "انواع نوآوری",
// sortable: true,
// width: "140px",
// },
// { key: "innovation", label: "میزان نوآوری", sortable: true, width: "120px" },
// {
// key: "person_executing",
// label: "مسئول اجرا",
// sortable: true,
// width: "140px",
// },
// {
// key: "excellent_observer",
// label: "ناطر عالی",
// sortable: true,
// width: "140px",
// },
// { key: "observer", label: "ناظر پروژه", sortable: true, width: "140px" },
// { key: "moderator", label: "مجری", sortable: true, width: "140px" },
// {
// key: "executive_phase",
// label: "فاز اجرایی",
// sortable: true,
// width: "140px",
// },
// { key: "start_date", label: "تاریخ شروع", sortable: true, width: "120px" },
// {
// key: "remaining_time",
// label: "زمان باقی مانده",
// sortable: true,
// width: "140px",
// computed: true,
// },
// {
// key: "end_date",
// label: "تاریخ پایان (برنامه‌ریزی)",
// sortable: true,
// width: "160px",
// },
// {
// key: "renewed_duration",
// label: "مدت زمان تمدید",
// sortable: true,
// width: "140px",
// },
// {
// key: "done_date",
// label: "تاریخ پایان (واقعی)",
// sortable: true,
// width: "160px",
// },
// {
// key: "deviation_from_program",
// label: "متوسط انحراف برنامه‌ای",
// sortable: true,
// width: "160px",
// },
// {
// key: "approved_budget",
// label: "بودجه مصوب",
// sortable: true,
// width: "150px",
// },
// {
// key: "budget_spent",
// label: "بودجه صرف شده",
// sortable: true,
// width: "150px",
// },
// {
// key: "cost_deviation",
// label: "متوسط انحراف هزینه‌ای",
// sortable: true,
// width: "160px",
// },
// ];
const columns: ColumnDef[] = [
{ key: "idea_title", label: "عنوان ایده", sortable: true, width: "200px" },
{ key: "idea_axis", label: "محور ایده", sortable: true, width: "160px" },
{
key: "idea_current_status_description",
label: "توضیح وضعیت فعلی ایده",
sortable: true,
width: "220px",
},
{
key: "idea_description",
label: "شرح ایده",
sortable: true,
width: "200px",
},
{
key: "full_name",
label: "نام و نام خانوادگی",
sortable: true,
width: "160px",
},
{
key: "idea_status",
label: "وضعیت ایده",
sortable: true,
width: "260px",
},
{
key: "personnel_number",
label: "شماره پرسنلی",
sortable: true,
width: "140px",
},
{
key: "innovator_team_members",
label: "اعضای تیم نوآور",
sortable: true,
width: "200px",
},
{
key: "idea_registration_date",
label: "تاریخ ثبت ایده",
sortable: true,
width: "160px",
},
{ key: "deputy", label: "معاونت مربوطه", sortable: true, width: "160px" },
{ key: "management", label: "مدیریت", sortable: true, width: "140px" },
{
key: "idea_execution_benefits",
label: "مزایای اجرای ایده",
sortable: true,
width: "220px",
},
{
key: "innovation_type",
label: "نوع نوآوری",
sortable: true,
width: "160px",
},
{
key: "idea_originality",
label: "میزان اصالت ایده",
sortable: true,
width: "160px",
},
{
key: "idea_income",
label: "درآمد حاصل از ایده",
sortable: true,
width: "160px",
},
{
key: "process_improvements",
label: "بهبودهای فرآیندی",
sortable: true,
width: "180px",
},
];
// idea_income , idea_registration_date , idea_originality , personnel_number
export function ManageIdeasTechPage() {
const [projects, setProjects] = useState<ProjectData[]>([]);
const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(25);
const [hasMore, setHasMore] = useState(true);
const [totalCount, setTotalCount] = useState(0);
const [actualTotalCount, setActualTotalCount] = useState(0);
const [sortConfig, setSortConfig] = useState<SortConfig>({
field: "idea_title",
direction: "asc",
});
const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false);
const fetchProjects = async (reset = false) => {
// Prevent concurrent API calls
if (fetchingRef.current) {
return;
}
try {
fetchingRef.current = true;
if (reset) {
setLoading(true);
setCurrentPage(1);
} else {
setLoadingMore(true);
}
const pageToFetch = reset ? 1 : currentPage;
const fetchableColumns = columns.filter((c) => !c.computed);
const outputFields = fetchableColumns.map((c) => c.apiField ?? c.key);
const sortCol = columns.find((c) => c.key === sortConfig.field);
const sortField = sortCol?.computed
? undefined
: (sortCol?.apiField ?? sortCol?.key);
const response = await apiService.select({
ProcessName: "idea",
OutputFields: outputFields,
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
Sorts: sortField ? [[sortField, sortConfig.direction]] : [],
Conditions: [],
});
if (response.state === 0) {
// Parse the JSON string from the API response
const dataString = response.data;
if (dataString && typeof dataString === "string") {
try {
const parsedData = JSON.parse(dataString);
if (Array.isArray(parsedData)) {
if (reset) {
setProjects(parsedData);
setTotalCount(parsedData.length);
} else {
setProjects((prev) => [...prev, ...parsedData]);
setTotalCount((prev) => prev + parsedData.length);
}
// Check if there are more items to load
setHasMore(parsedData.length === pageSize);
} else {
if (reset) {
setProjects([]);
setTotalCount(0);
}
setHasMore(false);
}
} catch (parseError) {
console.error("Error parsing project data:", parseError);
if (reset) {
setProjects([]);
setTotalCount(0);
}
setHasMore(false);
}
} else {
if (reset) {
setProjects([]);
setTotalCount(0);
}
setHasMore(false);
}
} else {
toast.error(response.message || "خطا در دریافت اطلاعات پروژه‌ها");
if (reset) {
setProjects([]);
setTotalCount(0);
}
setHasMore(false);
}
} catch (error) {
console.error("Error fetching projects:", error);
toast.error("خطا در دریافت اطلاعات پروژه‌ها");
if (reset) {
setProjects([]);
setTotalCount(0);
}
setHasMore(false);
} finally {
setLoading(false);
setLoadingMore(false);
fetchingRef.current = false;
}
};
const loadMore = useCallback(() => {
if (!loadingMore && hasMore && !loading) {
setCurrentPage((prev) => prev + 1);
}
}, [loadingMore, hasMore, loading]);
useEffect(() => {
fetchProjects(true);
fetchTotalCount();
}, [sortConfig]);
useEffect(() => {
if (currentPage > 1) {
fetchProjects(false);
}
}, [currentPage]);
// Infinite scroll observer
useEffect(() => {
const scrollContainer = document.querySelector(".overflow-auto");
const handleScroll = () => {
if (!scrollContainer || !hasMore || loadingMore) return;
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
// Trigger load more when scrolled to 90% of the container
if (scrollPercentage >= 0.9) {
loadMore();
}
};
if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll);
}
return () => {
if (scrollContainer) {
scrollContainer.removeEventListener("scroll", handleScroll);
}
};
}, [loadMore, hasMore, loadingMore]);
const handleSort = (field: string) => {
fetchingRef.current = false; // Reset fetching state on sort
setSortConfig((prev) => ({
field,
direction:
prev.field === field && prev.direction === "asc" ? "desc" : "asc",
}));
setCurrentPage(1);
setProjects([]);
setHasMore(true);
};
const fetchTotalCount = async () => {
try {
const response = await apiService.select({
ProcessName: "idea",
OutputFields: ["count(idea_no)"],
Conditions: [],
});
if (response.state === 0) {
const dataString = response.data;
if (dataString && typeof dataString === "string") {
try {
const parsedData = JSON.parse(dataString);
if (Array.isArray(parsedData) && parsedData[0]) {
setActualTotalCount(parsedData[0].project_no_count || 0);
}
} catch (parseError) {
console.error("Error parsing count data:", parseError);
}
}
}
} catch (error) {
console.error("Error fetching total count:", error);
}
};
const formatCurrency = (amount: string | number) => {
if (!amount) return "0 ریال";
// Remove commas and convert to number
const numericAmount =
typeof amount === "string"
? parseFloat(amount.replace(/,/g, ""))
: amount;
if (isNaN(numericAmount)) return "0 ریال";
return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
};
// const formatNumber = (value: string | number) => {
// if (value === undefined || value === null || value === "") return "0";
// const numericValue = typeof value === "string" ? Number(value) : value;
// if (Number.isNaN(numericValue)) return "0";
// return new Intl.NumberFormat("fa-IR").format(numericValue as number);
// };
const toPersianDigits = (input: string | number): string => {
const str = String(input);
const map: Record<string, string> = {
"0": "۰",
"1": "۱",
"2": "۲",
"3": "۳",
"4": "۴",
"5": "۵",
"6": "۶",
"7": "۷",
"8": "۸",
"9": "۹",
};
return str.replace(/[0-9]/g, (d) => map[d] ?? d);
};
// ----- Jalali <-> Gregorian conversion helpers (lightweight, local) -----
const isJalaliDateString = (raw: string): boolean => {
return /^(\d{4})[\/](\d{1,2})[\/](\d{1,2})$/.test(raw.trim());
};
const div = (a: number, b: number) => ~~(a / b);
const jalaliToJDN = (jy: number, jm: number, jd: number): number => {
jy = jy - (jy >= 0 ? 474 : 473);
const cycle = 1029983;
const yCycle = 474 + (jy % 2820);
const jdn =
jd +
(jm <= 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186) +
div((yCycle * 682 - 110) as number, 2816) +
(yCycle - 1) * 365 +
div(jy, 2820) * cycle +
(1948320 - 1);
return jdn;
};
const jdnToGregorian = (jdn: number): [number, number, number] => {
let j = jdn + 32044;
const g = div(j, 146097);
const dg = j % 146097;
const c = div((div(dg, 36524) + 1) * 3, 4);
const dc = dg - c * 36524;
const b = div(dc, 1461);
const db = dc % 1461;
const a = div((div(db, 365) + 1) * 3, 4);
const da = db - a * 365;
const y = g * 400 + c * 100 + b * 4 + a;
const m = div(da * 5 + 308, 153) - 2;
const d = da - div((m + 4) * 153, 5) + 122;
const year = y - 4800 + div(m + 2, 12);
const month = ((m + 2) % 12) + 1;
const day = d + 1;
return [year, month, day];
};
const jalaliToGregorianDate = (jy: number, jm: number, jd: number): Date => {
const jdn = jalaliToJDN(jy, jm, jd);
const [gy, gm, gd] = jdnToGregorian(jdn);
return new Date(gy, gm - 1, gd);
};
const parseToDate = (value: string | null): Date | null => {
if (!value) return null;
const raw = String(value).trim();
if (isJalaliDateString(raw)) {
const [jy, jm, jd] = raw.split("/").map((s) => Number(s));
if ([jy, jm, jd].some((n) => Number.isNaN(n))) return null;
return jalaliToGregorianDate(jy, jm, jd);
}
const tryDate = new Date(raw);
return Number.isNaN(tryDate.getTime()) ? null : tryDate;
};
const getTodayMidnight = (): Date => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
};
const calculateRemainingDays = (end: string | null): number | null => {
if (!end) return null; // if either missing
const endDate = parseToDate(end);
if (!endDate) return null;
const today = getTodayMidnight();
const MS_PER_DAY = 24 * 60 * 60 * 1000;
const diff = Math.round((endDate.getTime() - today.getTime()) / MS_PER_DAY);
return diff;
};
const formatDate = (dateString: string | null) => {
if (!dateString || dateString === "null" || dateString.trim() === "") {
return "-";
}
// If API already returns Jalali like 1404/05/30, just convert digits
const raw = String(dateString).trim();
const jalaliPattern = /^(\d{4})[\/](\d{1,2})[\/](\d{1,2})$/;
const jalaliMatch = raw.match(jalaliPattern);
if (jalaliMatch) {
const [, y, m, d] = jalaliMatch;
const mm = m.padStart(2, "0");
const dd = d.padStart(2, "0");
return toPersianDigits(`${y}/${mm}/${dd}`);
}
// Otherwise, try to parse and render Persian calendar
try {
const parsed = new Date(raw);
if (isNaN(parsed.getTime())) return "-";
return new Intl.DateTimeFormat("fa-IR-u-ca-persian", {
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(parsed);
} catch {
return "-";
}
};
const phaseColors: Record<string, string> = {
"تحقیق و توسعه": "#FFD700", // Yellow
آزمایش: "#1E90FF", // Blue
تولید: "#32CD32", // Green
default: "#ccc", // Fallback gray
};
const getImportanceColor = (importance: string) => {
switch (importance?.toLowerCase()) {
case "تایید شده":
return "#3AEA83"; // سبز
case "در حال بررسی":
return "#69C8EA"; // آبی
case "رد شده":
return "#F76276"; // قرمز
case "اجرا شده":
return "#FBBF24"; // زرد/نارنجی
default:
return "#6B7280"; // خاکستری پیش‌فرض
}
};
// idea_income , idea_registration_date , idea_originality , personnel_number
const renderCellContent = (item: ProjectData, column: ColumnDef) => {
const apiField = column.apiField ?? column.key;
const value = (item as any)[apiField];
switch (column.key) {
case "remaining_time": {
const days = calculateRemainingDays(item.end_date);
if (days == null) {
return <span className="text-gray-300">-</span>;
}
const color = days > 0 ? "#3AEA83" : days < 0 ? "#F76276" : undefined;
return (
<span
dir="ltr"
className="font-medium flex justify-end gap-1 items-center"
style={{ color }}
>
<span>روز</span> {toPersianDigits(days)}
</span>
);
}
// case "strategic_theme":
// case "value_technology_and_innovation":
// case "type_of_innovation":
// case "innovation":
// return (
// <span className="inline-flex items-center justify-end flex-row-reverse gap-2 w-full">
// <span className="text-gray-300">{String(value) || "-"}</span>
// <span
// style={{
// backgroundColor: `${column.key === "strategic_theme" ? "#6D53FB" : column.key === "value_technology_and_innovation" ? "#A757FF" : column.key === "type_of_innovation" ? "#E884CE" : "#C3BF8B"}`,
// }}
// className="inline-block w-2 h-2 rounded-full bg-emerald-400"
// />
// </span>
// );
case "idea_income":
return (
<span className="font-medium text-emerald-400">
{formatCurrency(String(value))}
</span>
);
case "personnel_number":
// case "idea_originality":
return (
<span className="text-gray-300">{formatNumber(value as any)} </span>
);
case "idea_registration_date":
return (
<span className="text-gray-300">{formatDate(String(value))}</span>
);
case "project_no":
return (
<Badge
variant="outline"
className="font-mono text-emerald-400 border-emerald-500/50"
>
{String(value)}
</Badge>
);
case "idea_title":
return <span className="font-medium text-white">{String(value)}</span>;
case "idea_status":
return (
<Badge
variant="outline"
className="font-medium border-2"
style={{
color: getImportanceColor(String(value)),
borderColor: getImportanceColor(String(value)),
backgroundColor: `${getImportanceColor(String(value))}20`,
}}
>
{String(value)}
</Badge>
);
default:
return (
<span className="text-gray-300">
{(value && String(value)) || "-"}
</span>
);
}
};
const totalPages = Math.ceil(totalCount / pageSize);
return (
<DashboardLayout title="مدیریت ایده های فناوری و نوآوری">
<div className="p-6 space-y-6">
{/* Data Table */}
<Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
<div className="relative">
<Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(100vh-200px)]">
<TableHeader className="sticky top-0 z-50 bg-[#3F415A]">
<TableRow className="bg-[#3F415A]">
{columns.map((column) => (
<TableHead
key={column.key}
className="text-right font-persian whitespace-nowrap text-gray-200 font-medium bg-[#3F415A] sticky top-0 z-20"
style={{ width: column.width }}
>
{column.sortable ? (
<button
onClick={() => handleSort(column.key)}
className="flex items-center gap-2"
>
<span>{column.label}</span>
{sortConfig.field === column.key ? (
sortConfig.direction === "asc" ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)
) : (
<div className="w-4 h-4" />
)}
</button>
) : (
column.label
)}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
// Skeleton loading rows (compact)
Array.from({ length: 20 }).map((_, index) => (
<TableRow
key={`skeleton-${index}`}
className="text-sm leading-tight h-8"
>
{columns.map((column) => (
<TableCell
key={column.key}
className="text-right whitespace-nowrap border-emerald-500/20 py-1 px-2"
>
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 bg-gray-600 rounded-full animate-pulse" />
<div
className="h-2.5 bg-gray-600 rounded animate-pulse"
style={{ width: `${Math.random() * 60 + 40}%` }}
/>
</div>
</TableCell>
))}
</TableRow>
))
) : projects.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center py-8"
>
<span className="text-gray-400 font-persian">
هیچ پروژهای یافت نشد
</span>
</TableCell>
</TableRow>
) : (
projects.map((project, index) => (
<TableRow
key={`${project.project_no}-${index}`}
className="text-sm leading-tight h-8"
>
{columns.map((column) => (
<TableCell
key={column.key}
className="text-right whitespace-nowrap border-emerald-500/20 py-1 px-2"
>
{renderCellContent(project, column)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* Infinite scroll trigger */}
<div ref={observerRef} className="h-auto">
{loadingMore && (
<div className="flex items-center justify-center py-1">
<div className="flex items-center gap-2">
<RefreshCw className="w-4 h-4 animate-spin text-emerald-400" />
<span className="font-persian text-gray-300 text-xs"></span>
</div>
</div>
)}
</div>
</CardContent>
{/* Footer */}
<div className="p-4 bg-gray-700/50">
<div className="flex items-center justify-between text-sm text-gray-300 font-persian">
<span>کل پروژهها: {formatNumber(actualTotalCount)}</span>
</div>
</div>
</Card>
</div>
</DashboardLayout>
);
}

View File

@ -1,40 +1,25 @@
import {
Box,
Building2,
ChevronDown,
ChevronRight,
FolderKanban,
GalleryVerticalEnd,
Globe,
LayoutDashboard,
Leaf,
Lightbulb,
LogOut,
MonitorSmartphone,
Package,
Settings,
Star,
Workflow,
} from "lucide-react";
import React, { useState } from "react"; import React, { useState } from "react";
import { Link, useLocation } from "react-router"; import { Link, useLocation } from "react-router";
import { cn } from "~/lib/utils";
import { InogenLogo } from "~/components/ui/brand-logo";
import { useAuth } from "~/contexts/auth-context"; import { useAuth } from "~/contexts/auth-context";
import { import { cn } from "~/lib/utils";
GalleryVerticalEnd,
LayoutDashboard,
FolderOpen,
Users,
BarChart3,
Settings,
ChevronLeft,
ChevronDown,
FileText,
Calendar,
Bell,
User,
Database,
Shield,
HelpCircle,
LogOut,
ChevronRight,
Refrigerator,
} from "lucide-react";
import {
FolderKanban,
Box,
Package,
Workflow,
MonitorSmartphone,
Leaf,
Building2,
Globe,
Lightbulb,
Star,
} from "lucide-react";
interface SidebarProps { interface SidebarProps {
isCollapsed?: boolean; isCollapsed?: boolean;
@ -111,7 +96,7 @@ const menuItems: MenuItem[] = [
id: "ideas", id: "ideas",
label: "ایده‌های فناوری و نوآوری", label: "ایده‌های فناوری و نوآوری",
icon: Lightbulb, icon: Lightbulb,
href: "/dashboard/ideas", href: "/dashboard/manage-ideas-tech",
}, },
{ {
id: "top-innovations", id: "top-innovations",
@ -153,7 +138,7 @@ export function Sidebar({
menuItems.forEach((item) => { menuItems.forEach((item) => {
if (item.children) { if (item.children) {
const hasActiveChild = item.children.some( const hasActiveChild = item.children.some(
(child) => child.href && location.pathname === child.href, (child) => child.href && location.pathname === child.href
); );
if (hasActiveChild) { if (hasActiveChild) {
newExpandedItems.push(item.id); newExpandedItems.push(item.id);
@ -174,7 +159,7 @@ export function Sidebar({
const item = menuItems.find((menuItem) => menuItem.id === itemId); const item = menuItems.find((menuItem) => menuItem.id === itemId);
if (item?.children) { if (item?.children) {
const hasActiveChild = item.children.some( const hasActiveChild = item.children.some(
(child) => child.href && location.pathname === child.href, (child) => child.href && location.pathname === child.href
); );
// Don't collapse if a child is active // Don't collapse if a child is active
if (hasActiveChild) { if (hasActiveChild) {
@ -192,7 +177,7 @@ export function Sidebar({
if (href && location.pathname === href) return true; if (href && location.pathname === href) return true;
if (children) { if (children) {
return children.some( return children.some(
(child) => child.href && location.pathname === child.href, (child) => child.href && location.pathname === child.href
); );
} }
return false; return false;
@ -204,7 +189,7 @@ export function Sidebar({
expandedItems.includes(item.id) || expandedItems.includes(item.id) ||
(item.children && (item.children &&
item.children.some( item.children.some(
(child) => child.href && location.pathname === child.href, (child) => child.href && location.pathname === child.href
)); ));
const hasChildren = item.children && item.children.length > 0; const hasChildren = item.children && item.children.length > 0;
@ -230,15 +215,14 @@ export function Sidebar({
? " text-emerald-400 border-r-2 border-emerald-400" ? " text-emerald-400 border-r-2 border-emerald-400"
: "text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300", : "text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300",
isCollapsed && level === 0 && "justify-center px-2", isCollapsed && level === 0 && "justify-center px-2",
item.id === "logout" && item.id === "logout" && "hover:bg-red-500/10 hover:text-red-400"
"hover:bg-red-500/10 hover:text-red-400",
)} )}
> >
<div className="flex items-center gap-3 min-w-0 flex-1"> <div className="flex items-center gap-3 min-w-0 flex-1">
<ItemIcon <ItemIcon
className={cn( className={cn(
"w-5 h-5 flex-shrink-0", "w-5 h-5 flex-shrink-0",
isActive ? "text-emerald-400" : "text-current", isActive ? "text-emerald-400" : "text-current"
)} )}
/> />
{!isCollapsed && ( {!isCollapsed && (
@ -259,7 +243,7 @@ export function Sidebar({
<ChevronDown <ChevronDown
className={cn( className={cn(
"w-4 h-4 transition-transform duration-200", "w-4 h-4 transition-transform duration-200",
isExpanded ? "rotate-180" : "rotate-0", isExpanded ? "rotate-180" : "rotate-0"
)} )}
/> />
)} )}
@ -274,9 +258,9 @@ export function Sidebar({
// Disable pointer cursor when child is active (cannot collapse) // Disable pointer cursor when child is active (cannot collapse)
item.children && item.children &&
item.children.some( item.children.some(
(child) => child.href && location.pathname === child.href, (child) => child.href && location.pathname === child.href
) && ) &&
"cursor-not-allowed", "cursor-not-allowed"
)} )}
onClick={handleClick} onClick={handleClick}
> >
@ -288,15 +272,14 @@ export function Sidebar({
? " text-emerald-400 border-r-2 border-emerald-400" ? " text-emerald-400 border-r-2 border-emerald-400"
: "text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300", : "text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300",
isCollapsed && level === 0 && "justify-center px-2", isCollapsed && level === 0 && "justify-center px-2",
item.id === "logout" && item.id === "logout" && "hover:bg-red-500/10 hover:text-red-400"
"hover:bg-red-500/10 hover:text-red-400",
)} )}
> >
<div className="flex items-center gap-3 min-w-0 flex-1"> <div className="flex items-center gap-3 min-w-0 flex-1">
<ItemIcon <ItemIcon
className={cn( className={cn(
"w-5 h-5 flex-shrink-0", "w-5 h-5 flex-shrink-0",
isActive ? "text-emerald-400" : "text-current", isActive ? "text-emerald-400" : "text-current"
)} )}
/> />
{!isCollapsed && ( {!isCollapsed && (
@ -322,10 +305,10 @@ export function Sidebar({
item.children && item.children &&
item.children.some( item.children.some(
(child) => (child) =>
child.href && location.pathname === child.href, child.href && location.pathname === child.href
) )
? "text-emerald-400" ? "text-emerald-400"
: "text-current", : "text-current"
)} )}
/> />
)} )}
@ -360,7 +343,7 @@ export function Sidebar({
className={cn( className={cn(
" backdrop-blur-sm h-full flex flex-col transition-all duration-300", " backdrop-blur-sm h-full flex flex-col transition-all duration-300",
isCollapsed ? "w-16" : "w-64", isCollapsed ? "w-16" : "w-64",
className, className
)} )}
> >
{/* Header */} {/* Header */}
@ -424,7 +407,7 @@ export function Sidebar({
<ChevronRight <ChevronRight
className={cn( className={cn(
"w-4 h-4 text-gray-400 transition-transform duration-200", "w-4 h-4 text-gray-400 transition-transform duration-200",
isCollapsed ? "rotate-180" : "rotate-0", isCollapsed ? "rotate-180" : "rotate-0"
)} )}
/> />
{!isCollapsed && ( {!isCollapsed && (

View File

@ -1,4 +1,4 @@
import { type RouteConfig, index, route } from "@react-router/dev/routes"; import { type RouteConfig, route } from "@react-router/dev/routes";
export default [ export default [
route("login", "routes/login.tsx"), route("login", "routes/login.tsx"),
@ -21,6 +21,7 @@ export default [
"routes/digital-innovation-page.tsx" "routes/digital-innovation-page.tsx"
), ),
route("dashboard/ecosystem", "routes/ecosystem.tsx"), route("dashboard/ecosystem", "routes/ecosystem.tsx"),
route("dashboard/manage-ideas-tech", "routes/manage-ideas-tech-page.tsx"),
route("404", "routes/404.tsx"), route("404", "routes/404.tsx"),
route("unauthorized", "routes/unauthorized.tsx"), route("unauthorized", "routes/unauthorized.tsx"),
route("*", "routes/$.tsx"), // Catch-all route for 404s route("*", "routes/$.tsx"), // Catch-all route for 404s

View File

@ -0,0 +1,17 @@
import { ProtectedRoute } from "~/components/auth/protected-route";
import { ManageIdeasTechPage } from "~/components/dashboard/project-management/mange-ideas-tech-page";
export function meta() {
return [
{ title: "مدیریت فنواری و ایده ها" },
{ name: "description", content: "مدیریت پروژه‌های فناوری و نوآوری" },
];
}
export default function ManageIdeasTech() {
return (
<ProtectedRoute requireAuth={true}>
<ManageIdeasTechPage />
</ProtectedRoute>
);
}