From ef2cf01495cd82846d1ce3295508d5e3fb396fa9 Mon Sep 17 00:00:00 2001 From: saeed0920 Date: Thu, 11 Sep 2025 11:03:27 +0330 Subject: [PATCH] add new page (#9) Reviewed-on: https://git.pelekan.org/Saeed0920/inogen/pulls/9 Co-authored-by: saeed0920 Co-committed-by: saeed0920 --- .../product-innovation-page.tsx | 1116 +++++++++++++++++ app/components/ui/funnel-chart.test.tsx | 39 + app/components/ui/funnel-chart.tsx | 95 ++ app/routes.ts | 4 + .../innovation-basket.product-innovation.tsx | 17 + 5 files changed, 1271 insertions(+) create mode 100644 app/components/dashboard/project-management/product-innovation-page.tsx create mode 100644 app/components/ui/funnel-chart.test.tsx create mode 100644 app/components/ui/funnel-chart.tsx create mode 100644 app/routes/innovation-basket.product-innovation.tsx diff --git a/app/components/dashboard/project-management/product-innovation-page.tsx b/app/components/dashboard/project-management/product-innovation-page.tsx new file mode 100644 index 0000000..962df80 --- /dev/null +++ b/app/components/dashboard/project-management/product-innovation-page.tsx @@ -0,0 +1,1116 @@ +import { + ArrowDownCircle, + ArrowUpCircle, + Building2, + ChevronDown, + ChevronUp, + CirclePause, + DollarSign, + Funnel, + Loader2, + PickaxeIcon, + RefreshCw, + TrendingUp, + UserIcon, + UsersIcon, + Wrench, +} from "lucide-react"; +import moment from "moment-jalaali"; +import { useCallback, useEffect, useRef, useState } from "react"; +import toast from "react-hot-toast"; +import { Badge } from "~/components/ui/badge"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Checkbox } from "~/components/ui/checkbox"; +import { CustomBarChart } from "~/components/ui/custom-bar-chart"; +import { FunnelChart } from "~/components/ui/funnel-chart"; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; +import { Label } from "~/components/ui/label"; +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"; +import { Skeleton } from "~/components/ui/skeleton"; + +moment.loadPersian({ usePersianDigits: true }); + +interface ProjectData { + project_no: string; + project_id: string; + title: string; + project_status: string; + project_rating: string; + project_description: string; + developed_technology_type: string; + obtained_standard_title: string; + knowledge_based_certificate_obtained: string; + knowledge_based_certificate_number: string; + certificate_obtain_date: string; + issuing_authority: string; +} + +interface ProductInnovationStats { + new_products_revenue_share: number; + new_products_revenue_share_percent: number; + new_products_export: number; + import_impact: number; + all_funnel: number; + successful_sample_funnel: number; + successful_products_funnel: number; + successful_improvement_or_change_funnel: number; + new_product_funnel: number; + count_innovation_construction_inside_projects: number; + average_project_score: number; +} + +interface ProductInnovationData { + WorkflowID: number; + ValueP1215S1887ValueID: number; + ValueP1215S1887StageID: number; + project_id: string; + project_no: string; + title: string; + project_status: projectStatus; + project_rating: string; + project_description: string; + developed_technology_type: string; + obtained_standard_title: string; + knowledge_based_certificate_obtained: string; + knowledge_based_certificate_number: string; + certificate_obtain_date: string; + issuing_authority: string; +} + +interface SortConfig { + field: string; + direction: "asc" | "desc"; +} + +enum projectStatus { + propozal = "پروپوزال", + contract = "پیشنویس قرارداد", + inprogress = "در حال انجام", + stop = "متوقف شده", + mafasa = "مرحله مفاصا", + finish = "پایان یافته", + notstarted = "شروع نشده", + delayed = "تأخیر دارد", +} + +const columns = [ + { key: "project_no", label: "شماره پروژه", sortable: true, width: "140px" }, + { key: "title", label: "عنوان پروژه", sortable: true, width: "400px" }, + { + key: "project_status", + label: "وضعیت پروژه", + sortable: true, + width: "140px", + }, + { + key: "project_rating", + label: "امتیاز پروژه", + sortable: true, + width: "140px", + }, + { key: "details", label: "جزئیات پروژه", sortable: false, width: "140px" }, +]; + +export function ProductInnovationPage() { + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingMore, setLoadingMore] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize] = useState(20); + const [hasMore, setHasMore] = useState(true); + const [totalCount, setTotalCount] = useState(0); + const [actualTotalCount, setActualTotalCount] = useState(0); + const [statsLoading, setStatsLoading] = useState(false); + const [stats, setStats] = useState({ + new_products_revenue_share: 0, + new_products_revenue_share_percent: 0, + new_products_export: 0, + import_impact: 0, + all_funnel: 0, + successful_sample_funnel: 0, + successful_products_funnel: 0, + successful_improvement_or_change_funnel: 0, + new_product_funnel: 0, + count_innovation_construction_inside_projects: 0, + average_project_score: 0, + }); + const [sortConfig, setSortConfig] = useState({ + field: "start_date", + direction: "asc", + }); + const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); + const [selectedProjectDetails, setSelectedProjectDetails] = + useState(null); + const [popupStats, setPopupStats] = useState({ + new_products_export: 0, + new_products_export_percent: 0, + import_impact: 0, + import_impact_percent: 0, + }); + const [exportChartData, setExportChartData] = useState([]); + const [allExportData, setAllExportData] = useState([]); + const [popupLoading, setPopupLoading] = useState(false); + + const [stateCard, setStateCard] = useState({ + revenueNewProducts: { + id: "revenueNewProducts", + title: "سهم از درآمد برای محصولات جدید", + value: "0", + description: "میلیون ریال", + descriptionPercent: "درصد به کل درآمد", + color: "text-[#3AEA83]", + percent : "0" + }, + newProductExports: { + id: "newProductExports", + title: "صادرات محصول جدید", + value: "0", + description: "میلیون ریال", + color: "text-[#3AEA83]", + }, + impactOnImports: { + id: "impactOnImports", + title: "تأثیر در واردات", + value: "0", + description: "میلیون ریال", + color: "text-[#F76276]", + }, + }); + + const observerRef = useRef(null); + const fetchingRef = useRef(false); + + + const handleProjectDetails = async (project: ProductInnovationData) => { + setSelectedProjectDetails(project); + setDetailsDialogOpen(true); + await fetchPopupData(project.project_id); + }; + + const fetchPopupData = async (projectId: string) => { + try { + setPopupLoading(true); + + // Fetch popup stats + const statsResponse = await apiService.call({ + innovation_product_popup_function1: { + project_id: projectId + } + }); + + if (statsResponse.state === 0) { + const statsData = JSON.parse(statsResponse.data); + if (statsData.innovation_product_popup_function1 && statsData.innovation_product_popup_function1[0]) { + setPopupStats(statsData.innovation_product_popup_function1[0]); + } + } + + // Fetch export chart data + const chartResponse = await apiService.select({ + ProcessName: "export_product_innovation", + OutputFields: [ + "product_title", + "full_season", + "sum(export_revenue)" + ], + GroupBy: ["product_title", "full_season"] + }); + + if (chartResponse.state === 0) { + const chartData = JSON.parse(chartResponse.data); + if (Array.isArray(chartData)) { + // Set all data for line chart + setAllExportData(chartData); + + // Filter data for the selected project (bar chart) + const filteredData = chartData.filter(item => + item.product_title === selectedProjectDetails?.title + ); + setExportChartData(filteredData); + } + } + + } catch (error) { + console.error("Error fetching popup data:", error); + } finally { + setPopupLoading(false); + } + }; + + const loadMore = useCallback(() => { + if (!loadingMore && hasMore && !loading) { + setCurrentPage((prev) => prev + 1); + } + }, [loadingMore, hasMore, loading]); + + const fetchProjects = async (reset = false) => { + if (fetchingRef.current) { + return; + } + + try { + fetchingRef.current = true; + + if (reset) { + setLoading(true); + setCurrentPage(1); + } else { + setLoadingMore(true); + } + + const pageToFetch = reset ? 1 : currentPage; + + const response = await apiService.select({ + ProcessName: "project", + OutputFields: [ + "project_id", + "project_no", + "title", + "project_status", + "project_rating", + "project_description", + "developed_technology_type", + "obtained_standard_title", + "knowledge_based_certificate_obtained", + "knowledge_based_certificate_number", + "certificate_obtain_date", + "issuing_authority", + ], + Sorts: [["start_date", "asc"]], + Conditions: [["type_of_innovation", "=", "نوآوری در محصول"]], + Pagination: { PageNumber: pageToFetch, PageSize: pageSize }, + }); + + if (response.state === 0) { + 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); + } + 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 fetchStats = async () => { + try { + setStatsLoading(true); + const raw = await apiService.call({ + innovation_product_function: {}, + }); + + let payload: any = JSON.parse(raw?.data); + const parseNum = (v: unknown): any => { + const convertNumber = typeof v === "number" ? Math.max(0, v) : 0; + if (v == null) return 0; + if (typeof v === "number") return convertNumber; + if (typeof v === "string") { + const cleaned = v.replace(/,/g, "").trim(); + const n = parseFloat(cleaned); + return isNaN(n) ? 0 : convertNumber; + } + return 0; + }; + + const data: Array = JSON.parse( + payload?.innovation_product_function + ); + const stats = data[0]; + const normalized: ProductInnovationStats = { + new_products_revenue_share: parseNum(stats?.new_products_revenue_share), + new_products_revenue_share_percent: parseNum(stats?.new_products_revenue_share_percent), + import_impact: parseNum(stats?.import_impact), + new_products_export: parseNum(stats?.new_products_export), + all_funnel: parseNum(stats?.all_funnel), + successful_sample_funnel: parseNum(stats?.successful_sample_funnel), + successful_products_funnel: parseNum(stats?.successful_products_funnel), + successful_improvement_or_change_funnel: parseNum(stats?.successful_improvement_or_change_funnel), + new_product_funnel: parseNum(stats?.new_product_funnel), + count_innovation_construction_inside_projects: parseNum(stats?.count_innovation_construction_inside_projects), + average_project_score: parseNum(stats?.average_project_score), + }; + + setStateCard((prev) => ({ + ...prev, + revenueNewProducts: { + ...prev.revenueNewProducts, + value: formatNumber(normalized.new_products_revenue_share), + percent: formatNumber(normalized.new_products_revenue_share_percent), + }, + impactOnImports: { + ...prev.impactOnImports, + value: formatNumber(normalized.import_impact), + }, + newProductExports: { + ...prev.newProductExports, + value: formatNumber(normalized.new_products_export), + }, + })); + + setStats(normalized); + } catch (error) { + console.error("Error fetching stats:", error); + } finally { + setStatsLoading(false); + } + }; + + useEffect(() => { + fetchProjects(true); + }, [sortConfig]); + + useEffect(() => { + fetchStats(); + }, []); + + useEffect(() => { + if (currentPage > 1) { + fetchProjects(false); + } + }, [currentPage]); + + useEffect(() => { + const scrollContainer = document.querySelector(".overflow-auto"); + + const handleScroll = () => { + if (!scrollContainer || !hasMore || loadingMore) return; + + const { scrollTop, scrollHeight, clientHeight } = scrollContainer; + const scrollPercentage = (scrollTop + clientHeight) / scrollHeight; + + 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; + setSortConfig((prev) => ({ + field, + direction: + prev.field === field && prev.direction === "asc" ? "desc" : "asc", + })); + setCurrentPage(1); + setProjects([]); + setHasMore(true); + }; + + + const formatCurrency = (amount: string | number) => { + if (!amount) return "0 ریال"; + const numericAmount = + typeof amount === "string" + ? parseFloat(amount.replace(/,/g, "")) + : amount; + if (isNaN(numericAmount)) return "0 ریال"; + return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال"; + }; + + // Transform data for line chart + const transformDataForLineChart = (data: any[]) => { + const seasons = [...new Set(data.map(item => item.full_season))].sort(); + const products = [...new Set(data.map(item => item.product_title))]; + + return seasons.map(season => { + const seasonData: any = { season }; + products.forEach(product => { + const productData = data.find(item => + item.product_title === product && item.full_season === season + ); + seasonData[product] = productData ? Math.round(productData.export_revenue_sum / 1000000) : 0; + }); + return seasonData; + }); + }; + + const getRatingColor = (rating: string | number) => { + const numRating = typeof rating === "string" ? parseInt(rating) : rating; + if (numRating >= 150) return "text-emerald-400"; + if (numRating >= 100) return "text-blue-400"; + return "text-red-400"; + }; + + const statusColor = (status: projectStatus): any => { + let el = null; + switch (status) { + case projectStatus.contract: + el = "teal"; + break; + case projectStatus.finish: + el = "info"; + break; + case projectStatus.stop: + el = "warning"; + break; + case projectStatus.inprogress: + el = "teal"; + break; + case projectStatus.mafasa: + el = "destructive"; + break; + case projectStatus.propozal: + el = "info"; + break; + case projectStatus.notstarted: + el = "secondary"; + break; + case projectStatus.delayed: + el = "destructive"; + break; + } + return el; + }; + + const renderCellContent = (item: ProductInnovationData, column: any) => { + const value = item[column.key as keyof ProductInnovationData]; + + switch (column.key) { + case "select": + return null; + case "details": + return ( + + ); + case "project_no": + return ( + + {String(value)} + + ); + case "title": + return {String(value)}; + case "project_status": + return ( +
+ + {String(value)} +
+ ); + case "project_rating": + return ( + + {formatNumber(String(value))} + + ); + default: + return {String(value) || "-"}; + } + }; + + return ( + +
+ {/* Stats Cards */} +
+
+ {/* Stats Grid */} +
+ {loading || statsLoading + ? // Loading skeleton for stats cards - matching new design + Array.from({ length: 3 }).map((_, index) => ( + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + )) + : Object.entries(stateCard).map(([key, card] , index) => ( + + +
+
+

+ {card.title} +

+
+
+
+
+

+ {card.value} + +

+

+ {card.description} +

+
+ {card?.percent && /} + {card?.percent &&
+

+ {card?.percent} + +

+

+ {card.descriptionPercent} +

+
} +
+
+
+ ))} +
+
+ + {/* Funnel Chart */} + + + + + +
+ + {/* Data Table */} + + +
+ + + + {columns.map((column) => ( + + {column.sortable ? ( + + ) : ( + column.label + )} + + ))} + + + + {loading ? ( + // Skeleton loading rows (compact) + Array.from({ length: 10 }).map((_, index) => ( + + {columns.map((column) => ( + +
+
+
+
+ + ))} + + )) + ) : projects.length === 0 ? ( + + + + هیچ پروژه‌ای یافت نشد + + + + ) : ( + projects.map((project, index) => ( + + {columns.map((column) => ( + + {renderCellContent(project, column)} + + ))} + + )) + )} + +
+
+ + {/* Infinite scroll trigger */} +
+ {loadingMore && ( +
+
+ + +
+
+ )} +
+
+ + {/* Footer */} +
+
+
+
+ کل پروژه ها :{formatNumber(stats?.count_innovation_construction_inside_projects)} +
+
+ +
+
+ + + + +
+
+
میانگین :‌
+
+ {formatNumber( + ((stats.average_project_score ?? 0) as number).toFixed?.(1) ?? 0 + )} +
+
+
+
+
+
+
+ + {/* Project Details Dialog */} + + + + + جزئیات پروژه + + + +
+ {/* Left Column - Project Description and Charts */} +
+ {/* Project Description */} +
+

+ {selectedProjectDetails?.title} +

+

+ {selectedProjectDetails?.project_description || "-"} +

+ + {/* Project Timeline */} +
+
+ ۱۴۰۴ + ۱۴۰۵ + ۱۴۰۶ + ۱۴۰۷ +
+
+
+
+ ثبت ایده +
+
+
+
+ تحلیل بازار +
+
+
+
+ توسعه +
+
+
+
+ تجاری سازی +
+
+
وضعیت فعلی: تحلیل بازار
+
+
+ + {/* Export Revenue Bar Chart */} +
+

ظرفیت صادر شده

+
+ {popupLoading ? ( +
+
در حال بارگذاری...
+
+ ) : exportChartData.length > 0 ? ( + { + // Sort by season order + const seasonOrder = ['بهار', 'تابستان', 'پاییز', 'زمستان']; + const getSeasonIndex = (season: string) => { + const year = season.split(' ')[1]; + const seasonName = season.split(' ')[0]; + return parseInt(year) * 4 + seasonOrder.indexOf(seasonName); + }; + return getSeasonIndex(a.full_season) - getSeasonIndex(b.full_season); + }) + .map(item => ({ + label: item.full_season, + value: Math.round(item.export_revenue_sum / 1000000), // Convert to millions + color: "bg-emerald-400", + labelColor: "text-white" + }))} + barHeight="h-6" + showAxisLabels={true} + /> + ) : ( +
+ داده‌ای برای نمایش وجود ندارد +
+ )} +
+
+ + {/* Export Revenue Line Chart */} +
+

ظرفیت صادر شده

+
+ {popupLoading ? ( +
+
در حال بارگذاری...
+
+ ) : allExportData.length > 0 ? ( + + + + + + + + {[...new Set(allExportData.map(item => item.product_title))].slice(0, 5).map((product, index) => { + const colors = ['#10B981', '#EF4444', '#3B82F6', '#F59E0B', '#8B5CF6']; + return ( + + ); + })} + + + ) : ( +
+ داده‌ای برای نمایش وجود ندارد +
+ )} +
+
+
+ + {/* Right Column - Stats Cards and Details */} +
+ {/* Stats Cards */} +
+
+

میزان صادارت محصول جدید

+ {popupLoading ? ( +
+
+
+
+
+ ) : ( + <> +
+ {formatNumber(popupStats.new_products_export)} +
+
میلیون ریال
+
+ {formatNumber(popupStats.new_products_export_percent)}% +
+
درصد به کل صادرات
+ + )} +
+ +
+

تاثیر در واردات

+ {popupLoading ? ( +
+
+
+
+
+ ) : ( + <> +
+ {formatNumber(popupStats.import_impact)} +
+
میلیون ریال
+
+ {formatNumber(popupStats.import_impact_percent)}% +
+
درصد صرفه جویی
+ + )} +
+
+ + {/* Technical Knowledge */} +
+

دانش فنی محصول جدید

+
+
+ + توسعه درونزا +
+
+ + همکاری فناورانه +
+
+ + سایر +
+
+
+ + {/* Standards */} +
+

استانداردهای ملی و بین المللی اخذ شده

+
+
+
+ استاندارد ملی شماره یک +
+
+
+ استاندارد بین المللی شماره یک +
+
+
+ استاندارد ملی شماره یک +
+
+
+ استاندارد ملی شماره یک +
+
+
+ + {/* Knowledge-based Certificate Button */} +
+ +
+
+
+
+
+ + ); +} + + \ No newline at end of file diff --git a/app/components/ui/funnel-chart.test.tsx b/app/components/ui/funnel-chart.test.tsx new file mode 100644 index 0000000..3d2f68c --- /dev/null +++ b/app/components/ui/funnel-chart.test.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { FunnelChart } from './funnel-chart'; + +const mockData = [ + { name: "تعداد کل", value: 250, label: "تعداد کل" }, + { name: "نمونه موفق", value: 130, label: "نمونه موفق" }, + { name: "محصولات موفق", value: 70, label: "محصولات موفق" }, + { name: "بهبود یا تغییر موفق", value: 80, label: "بهبود یا تغییر موفق" }, + { name: "محصول جدید", value: 50, label: "محصول جدید" }, +]; + +describe('FunnelChart', () => { + it('renders funnel chart with correct data', () => { + render(); + + expect(screen.getByText('قيف فرآیند پروژه ها')).toBeInTheDocument(); + expect(screen.getByText('۱۰۰%')).toBeInTheDocument(); + expect(screen.getByText('۲۵%')).toBeInTheDocument(); + expect(screen.getByText('ابتدا فرآیند')).toBeInTheDocument(); + expect(screen.getByText('انتها فرآیند')).toBeInTheDocument(); + }); + + it('displays funnel data values correctly', () => { + render(); + + expect(screen.getByText('۲۵۰')).toBeInTheDocument(); + expect(screen.getByText('۱۳۰')).toBeInTheDocument(); + expect(screen.getByText('۷۰')).toBeInTheDocument(); + expect(screen.getByText('۸۰')).toBeInTheDocument(); + expect(screen.getByText('۵۰')).toBeInTheDocument(); + }); + + it('renders without title when not provided', () => { + render(); + + expect(screen.queryByText('قيف فرآیند پروژه ها')).not.toBeInTheDocument(); + }); +}); diff --git a/app/components/ui/funnel-chart.tsx b/app/components/ui/funnel-chart.tsx new file mode 100644 index 0000000..87e4e83 --- /dev/null +++ b/app/components/ui/funnel-chart.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import { formatNumber } from "~/lib/utils"; + +interface FunnelData { + name: string; + value: number; + label: string; + percentage?: string; +} + +interface FunnelChartProps { + data: FunnelData[]; + title?: string; + className?: string; +} + +export function FunnelChart({ data, title, className = "" }: FunnelChartProps) { + const maxValue = Math.max(...data.map(d => d.value)); + const toPercent = (value: number) => { + if (!maxValue || maxValue <= 0) return 0; + return Math.round((value / maxValue) * 100); + }; + + return ( +
+ {title && ( +

+ {title} +

+ )} + +
+ {/* Start Process Line */} +
+
ابتدا فرآیند
+
+
+
۱۰۰%
+
+
+
+
+
+ + {/* Funnel Bars */} +
+ {data.map((item, index) => { + const widthPercentage = toPercent(item.value); + const barWidth = Math.max(20, widthPercentage); // Minimum 20% width + + return ( +
+
+ {item.label} +
+
+
+
+
+ + {item.value.toLocaleString('fa-IR')} + +
+
+
+
+
+ ); + })} +
+ + {/* End Process Line */} +
+
انتها فرآیند
+
+ {(() => { + const lastValue = data[data.length - 1]?.value ?? 0; + const percent = toPercent(lastValue); + return ( +
+
{formatNumber(percent)}%
+
+
+
+ ); + })()} +
+
+
+
+ ); +} diff --git a/app/routes.ts b/app/routes.ts index 7eff565..c5da8c8 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -7,6 +7,10 @@ export default [ route( "dashboard/innovation-basket/process-innovation", "routes/innovation-basket.process-innovation.tsx" + ), + route( + "dashboard/innovation-basket/product-innovation", + "routes/innovation-basket.product-innovation.tsx" ), route( "dashboard/innovation-basket/green-innovation", diff --git a/app/routes/innovation-basket.product-innovation.tsx b/app/routes/innovation-basket.product-innovation.tsx new file mode 100644 index 0000000..a3fab3f --- /dev/null +++ b/app/routes/innovation-basket.product-innovation.tsx @@ -0,0 +1,17 @@ +import { ProductInnovationPage } from "~/components/dashboard/project-management/product-innovation-page"; +import { ProtectedRoute } from "~/components/auth/protected-route"; + +export function meta() { + return [ + { title: "نوآوری محصول - سیستم مدیریت فناوری و نوآوری" }, + { name: "description", content: "مدیریت پروژه‌های نوآوری محصول" }, + ]; +} + +export default function ProductInnovation() { + return ( + + + + ); +} \ No newline at end of file